Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"test:metadata": "npm run compile && node ./test/runMetadataHeaderTests.js",
"test:formatter": "npm run compile && node ./test/runFormatterTests.js",
"test:corpus": "npm run compile && node ./test/runFormatterCorpusTests.js",
"test:corpus:helpers": "npm run compile && node ./test/runFormatterCorpusHelperTests.js",
"test:project": "npm run compile && node ./test/runProjectValidationTests.js",
"test:diagnostics": "npm run compile && node ./test/runDiagnosticsTests.js",
"validate": "npm run check && npm test",
Expand Down
129 changes: 129 additions & 0 deletions test/helpers/formatterCorpus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const fs = require('fs');
const path = require('path');

const { assert } = require('./runTest');
const { formatSql, watcomDialect, mssqlDialect, defaultOptions } = require('../formatter/helpers');

const corpusRoot = path.join(__dirname, '..', 'corpus');
const dialectsByCorpusDirectory = new Map([
['watcom', watcomDialect],
['mssql', mssqlDialect],
]);

function listFiles(directory, predicate) {
if (!fs.existsSync(directory)) {
return [];
}

return fs
.readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
const entryPath = path.join(directory, entry.name);

if (entry.isDirectory()) {
return listFiles(entryPath, predicate);
}

return entry.isFile() && predicate(entry.name) ? [entryPath] : [];
})
.sort((left, right) => left.localeCompare(right));
}

function listInputFixtures(directory) {
return listFiles(directory, (fileName) => fileName.endsWith('.input.sql'));
}

function listSqlFixtureFiles(directory) {
return listFiles(directory, (fileName) => fileName.endsWith('.sql'));
}

function getExpectedFixturePath(inputPath) {
return inputPath.replace(/\.input\.sql$/u, '.expected.sql');
}

function getInputFixturePath(expectedPath) {
return expectedPath.replace(/\.expected\.sql$/u, '.input.sql');
}

function getDialectForFixture(inputPath, root = corpusRoot) {
const relativePath = path.relative(root, inputPath);
const corpusDirectory = relativePath.split(path.sep)[0];
const dialect = dialectsByCorpusDirectory.get(corpusDirectory);

if (!dialect) {
throw new Error(`No corpus dialect is configured for '${corpusDirectory}'.`);
}

return dialect;
}

function getFixtureName(inputPath, root = corpusRoot) {
return path.relative(root, inputPath).replace(/\.input\.sql$/u, '');
}

function getRelativeFixturePath(fixturePath) {
return path.relative(process.cwd(), fixturePath);
}

function toCrLf(text) {
return text.replace(/\r\n|\r|\n/gu, '\n').replace(/\n/gu, '\r\n');
}

function readFixture(fixturePath) {
return fs.readFileSync(fixturePath, 'utf8');
}

function findCorpusFixturePairingIssues(directory) {
return listSqlFixtureFiles(directory).flatMap((fixturePath) => {
const relativeFixturePath = getRelativeFixturePath(fixturePath);

if (fixturePath.endsWith('.input.sql')) {
const expectedPath = getExpectedFixturePath(fixturePath);

return fs.existsSync(expectedPath)
? []
: [`Missing expected corpus fixture for ${relativeFixturePath}.`];
}

if (fixturePath.endsWith('.expected.sql')) {
const inputPath = getInputFixturePath(fixturePath);

return fs.existsSync(inputPath)
? []
: [`Missing input corpus fixture for ${relativeFixturePath}.`];
}

return [
`Unsupported corpus SQL fixture name ${relativeFixturePath}; use .input.sql or .expected.sql.`,
];
});
}

function listFixturePairs(root = corpusRoot) {
return listInputFixtures(root).map((inputPath) => ({
dialect: getDialectForFixture(inputPath, root),
expectedPath: getExpectedFixturePath(inputPath),
inputPath,
name: getFixtureName(inputPath, root),
}));
}

function assertCorpusFormatting({ dialect, expected, fixtureName, input, variant }) {
const result = formatSql(input, dialect, defaultOptions);
const idempotentResult = formatSql(expected, dialect, defaultOptions);
const testName = variant ? `${fixtureName} (${variant})` : fixtureName;

assert.equal(result.text, expected, `${testName} did not format to the expected output.`);
assert.equal(idempotentResult.text, expected, `${testName} expected output is not idempotent.`);
}

module.exports = {
assertCorpusFormatting,
corpusRoot,
findCorpusFixturePairingIssues,
getExpectedFixturePath,
getInputFixturePath,
listFixturePairs,
readFixture,
toCrLf,
};
1 change: 1 addition & 0 deletions test/runAllTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const suites = [
'./runMetadataHeaderTests',
'./runMetadataRegressionTests',
'./runFormatterTests',
'./runFormatterCorpusHelperTests',
'./runFormatterCorpusTests',
'./runDiagnosticsTests',
];
Expand Down
75 changes: 75 additions & 0 deletions test/runFormatterCorpusHelperTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const fs = require('fs');
const os = require('os');
const path = require('path');

const { assert, runTest } = require('./helpers/runTest');
const { findCorpusFixturePairingIssues, toCrLf } = require('./helpers/formatterCorpus');

function withTemporaryCorpus(testFn) {
const corpusDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlovely-corpus-'));

try {
testFn(corpusDirectory);
} finally {
fs.rmSync(corpusDirectory, { recursive: true, force: true });
}
}

function writeFixture(root, relativePath, contents = 'select 1;\n') {
const fixturePath = path.join(root, relativePath);

fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
fs.writeFileSync(fixturePath, contents, 'utf8');
}

runTest('formatter corpus helpers: accept paired fixtures', () => {
withTemporaryCorpus((corpusDirectory) => {
writeFixture(corpusDirectory, 'watcom/example.input.sql');
writeFixture(corpusDirectory, 'watcom/example.expected.sql');

assert.deepEqual(findCorpusFixturePairingIssues(corpusDirectory), []);
});
});

runTest('formatter corpus helpers: detect missing expected fixtures', () => {
withTemporaryCorpus((corpusDirectory) => {
writeFixture(corpusDirectory, 'watcom/missing-expected.input.sql');

assert.deepEqual(findCorpusFixturePairingIssues(corpusDirectory), [
`Missing expected corpus fixture for ${path.relative(
process.cwd(),
path.join(corpusDirectory, 'watcom/missing-expected.input.sql'),
)}.`,
]);
});
});

runTest('formatter corpus helpers: detect missing input fixtures', () => {
withTemporaryCorpus((corpusDirectory) => {
writeFixture(corpusDirectory, 'watcom/missing-input.expected.sql');

assert.deepEqual(findCorpusFixturePairingIssues(corpusDirectory), [
`Missing input corpus fixture for ${path.relative(
process.cwd(),
path.join(corpusDirectory, 'watcom/missing-input.expected.sql'),
)}.`,
]);
});
});

runTest('formatter corpus helpers: reject unsupported SQL fixture names', () => {
withTemporaryCorpus((corpusDirectory) => {
writeFixture(corpusDirectory, 'watcom/unsupported.sql');

assert.deepEqual(findCorpusFixturePairingIssues(corpusDirectory), [
`Unsupported corpus SQL fixture name ${path.relative(
process.cwd(),
path.join(corpusDirectory, 'watcom/unsupported.sql'),
)}; use .input.sql or .expected.sql.`,
]);
});
});

runTest('formatter corpus helpers: normalize mixed line endings to CRLF', () => {
assert.equal(toCrLf('first\nsecond\rthird\r\nfourth'), 'first\r\nsecond\r\nthird\r\nfourth');
});
125 changes: 8 additions & 117 deletions test/runFormatterCorpusTests.js
Original file line number Diff line number Diff line change
@@ -1,121 +1,12 @@
const fs = require('fs');
const path = require('path');

const { assert, runTest } = require('./helpers/runTest');
const { formatSql, watcomDialect, mssqlDialect, defaultOptions } = require('./formatter/helpers');

const corpusRoot = path.join(__dirname, 'corpus');
const dialectsByCorpusDirectory = new Map([
['watcom', watcomDialect],
['mssql', mssqlDialect],
]);

function listFiles(directory, predicate) {
if (!fs.existsSync(directory)) {
return [];
}

return fs
.readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
const entryPath = path.join(directory, entry.name);

if (entry.isDirectory()) {
return listFiles(entryPath, predicate);
}

return entry.isFile() && predicate(entry.name) ? [entryPath] : [];
})
.sort((left, right) => left.localeCompare(right));
}

function listInputFixtures(directory) {
return listFiles(directory, (fileName) => fileName.endsWith('.input.sql'));
}

function listSqlFixtureFiles(directory) {
return listFiles(directory, (fileName) => fileName.endsWith('.sql'));
}

function getExpectedFixturePath(inputPath) {
return inputPath.replace(/\.input\.sql$/u, '.expected.sql');
}

function getInputFixturePath(expectedPath) {
return expectedPath.replace(/\.expected\.sql$/u, '.input.sql');
}

function getDialectForFixture(inputPath) {
const relativePath = path.relative(corpusRoot, inputPath);
const corpusDirectory = relativePath.split(path.sep)[0];
const dialect = dialectsByCorpusDirectory.get(corpusDirectory);

if (!dialect) {
throw new Error(`No corpus dialect is configured for '${corpusDirectory}'.`);
}

return dialect;
}

function getFixtureName(inputPath) {
return path.relative(corpusRoot, inputPath).replace(/\.input\.sql$/u, '');
}

function getRelativeFixturePath(fixturePath) {
return path.relative(process.cwd(), fixturePath);
}

function toCrLf(text) {
return text.replace(/\r\n|\r|\n/gu, '\n').replace(/\n/gu, '\r\n');
}

function readFixture(fixturePath) {
return fs.readFileSync(fixturePath, 'utf8');
}

function findCorpusFixturePairingIssues(directory) {
return listSqlFixtureFiles(directory).flatMap((fixturePath) => {
const relativeFixturePath = getRelativeFixturePath(fixturePath);

if (fixturePath.endsWith('.input.sql')) {
const expectedPath = getExpectedFixturePath(fixturePath);

return fs.existsSync(expectedPath)
? []
: [`Missing expected corpus fixture for ${relativeFixturePath}.`];
}

if (fixturePath.endsWith('.expected.sql')) {
const inputPath = getInputFixturePath(fixturePath);

return fs.existsSync(inputPath)
? []
: [`Missing input corpus fixture for ${relativeFixturePath}.`];
}

return [
`Unsupported corpus SQL fixture name ${relativeFixturePath}; use .input.sql or .expected.sql.`,
];
});
}

function listFixturePairs() {
return listInputFixtures(corpusRoot).map((inputPath) => ({
dialect: getDialectForFixture(inputPath),
expectedPath: getExpectedFixturePath(inputPath),
inputPath,
name: getFixtureName(inputPath),
}));
}

function assertCorpusFormatting({ dialect, expected, fixtureName, input, variant }) {
const result = formatSql(input, dialect, defaultOptions);
const idempotentResult = formatSql(expected, dialect, defaultOptions);
const testName = variant ? `${fixtureName} (${variant})` : fixtureName;

assert.equal(result.text, expected, `${testName} did not format to the expected output.`);
assert.equal(idempotentResult.text, expected, `${testName} expected output is not idempotent.`);
}
const {
assertCorpusFormatting,
corpusRoot,
findCorpusFixturePairingIssues,
listFixturePairs,
readFixture,
toCrLf,
} = require('./helpers/formatterCorpus');

const fixturePairs = listFixturePairs();

Expand Down
Loading