Skip to content

Commit

Permalink
feat: improve *.sql.ts example #44
Browse files Browse the repository at this point in the history
  • Loading branch information
shah committed Oct 1, 2024
1 parent 0cf969f commit 071c77d
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 24 deletions.
2 changes: 2 additions & 0 deletions lib/cookbook/tap.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
This script demonstrates how to create a Test Anything Protocol (TAP) report using SQLite, following TAP version 14.
It includes multiple test cases, and subtests are formatted with indentation per TAP 14's subtest style.
The `tap.sql.ts` source code shows how to generate TAP views via TypeScript classes.
Key Concepts Demonstrated:
1. TAP Version Declaration: Specifies that TAP version 14 is being used.
2. TAP Plan: Indicates how many tests will be run (1..7 main tests, but one test contains subtests).
Expand Down
11 changes: 10 additions & 1 deletion lib/cookbook/tap.sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class SyntheticTestSuite extends tapNB.TestSuiteNotebook {
}

"Check if a user named 'Alice' exists in the table"() {
const checkUser = 'Alice';
const checkUser = "Alice";
return this.assertThat`
SELECT COUNT(*) AS user_count
FROM users
Expand All @@ -59,6 +59,14 @@ export class SyntheticTestSuite extends tapNB.TestSuiteNotebook {
})
.case(`name_length = 3`, `"Bob" has a 3-character name`);
}

// instead of `assertThat`, use `testCase` for full control
another_test() {
return this.testCase`
SELECT '# Skipping the check for user "Eve" as she is not expected in the dataset' AS tap_result
UNION ALL
SELECT 'ok - Skipping test for user "Eve" # SKIP: User "Eve" not expected in this dataset' AS tap_result`;
}
}

// this will be used by any callers who want to serve it as a CLI with SDTOUT
Expand All @@ -67,4 +75,5 @@ if (import.meta.main) {
new SyntheticTestSuite("synthetic_test_suite"),
);
console.log(SQL.join("\n"));
console.log(`SELECT * FROM synthetic_test_suite;`);
}
98 changes: 75 additions & 23 deletions lib/std/notebook/tap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function isNotTestCase() {
}

/**
* Decorator function to mark a test case as "skiped" in TAP.
* Decorator function to mark a test case as "skipped" in TAP.
* @returns A decorator function that adds metadata to the method.
*
* @example
Expand Down Expand Up @@ -111,7 +111,7 @@ export function skip() {
* aUsefulMethod() {
* // whatever you want
* }
*
*
* // because this is not decorated with @isNotTestCase() it will be a test case;
* // simple single-clause assertion with SQL expression with separate pass
* // and fail messages
Expand All @@ -136,21 +136,31 @@ export function skip() {
*
* // because this is not decorated with @isNotTestCase() it will be a test case;
* // multiple assertions, first with TAP `---` attributes
* last_test() {
* another_test() {
* return
* this.assertThat`
* SELECT age, LENGTH(name) AS name_length
* FROM users
* WHERE name = 'Bob'`
* .case(`age = 25`, `"Bob" is 25 years old`,
* { expr: `"Bob" is not 25 years old`,
* diags: {
* diags: {
* 'expected' : 25,
* 'got': "` || age || `", // `...` signifies "break out of SQL literal"
* }
* })
* .case(`name_length = 3`, `"Bob" has a 3-character name`);
* }
*
* // because this is not decorated with @isNotTestCase() it will be a test case;
* // instead of `assertThat`, use `testCase` for full control
* another_test() {
* return
* this.testCase`
* SELECT '# Skipping the check for user "Eve" as she is not expected in the dataset' AS tap_result
* UNION ALL
* SELECT 'ok - Skipping test for user "Eve" # SKIP: User "Eve" not expected in this dataset' AS tap_result`;
* }
* }
*
* // Create an instance of the notebook
Expand Down Expand Up @@ -179,14 +189,19 @@ export class TestSuiteNotebook
extends SurveilrSqlNotebook<SQLa.SqlEmitContext> {
readonly methodIsNotTestCase: Set<string> = new Set();
readonly tapSkipMethodNames: Set<string> = new Set();
constructor(readonly notebookName: string) {
constructor(
readonly notebookName: string,
readonly tapResultColName = "tap_result",
) {
super();
}

diags(d: Record<string, unknown>) {
return yaml.stringify(d, { skipInvalid: true });
}

// use this for more convenient, typical TAP output (use `testCase` method
// if you want more control)
get assertThat() {
const cases: {
readonly when: string | SQLa.SqlTextSupplier<SQLa.SqlEmitContext>;
Expand Down Expand Up @@ -226,6 +241,7 @@ export class TestSuiteNotebook
isAssertion: true,
SQL,
cases,
casesCount: () => cases.length,
case: (
when: typeof cases[number]["when"],
then: typeof cases[number]["then"],
Expand Down Expand Up @@ -273,7 +289,7 @@ export class TestSuiteNotebook
exprOrLit("ok", then, index)
} ELSE ${
exprOrLit("not ok", otherwise ?? then, index)
} END AS tap_result FROM test_case`,
} END AS ${this.tapResultColName} FROM test_case`,
}),
});
// use "builder" pattern to chain multiple `.case` so return the object
Expand All @@ -284,6 +300,26 @@ export class TestSuiteNotebook
};
}

// use this in your test cases when you want full control over TAP output
get testCase() {
return (
...args: Parameters<ReturnType<typeof SQLa.SQL<SQLa.SqlEmitContext>>>
) => {
const SQL = (tcName: string, tcIndex: number) => {
const testCaseBodySQL = SQLa.SQL<SQLa.SqlEmitContext>(this.ddlOptions)(
...args,
);
// deno-fmt-ignore
return this.SQL`
-- ${tcIndex}: ${tcName}
"${tcName}" AS (
${testCaseBodySQL.SQL(this.emitCtx)}
)`.SQL(this.emitCtx);
};
return { SQL, casesCount: () => 1 };
};
}

/**
* Generates SQL statements from TestSuiteNotebook subclasses' method-based "test case" notebooks.
*
Expand Down Expand Up @@ -320,40 +356,56 @@ export class TestSuiteNotebook
: arbitrarySqlStmtRegEx.test(String(c)) == false,
}).map(
async (c) => {
const assertion = await c.call() as ReturnType<
TestSuiteNotebook["assertThat"]
>;
const methodResult = await c.call();
const strategy: "assertThat" | "testCase" | "invalid" =
typeof methodResult === "object" && "isAssertion" in methodResult
? "assertThat"
: (typeof methodResult === "object" && "SQL" in methodResult
? "testCase"
: "invalid");
const body:
| ReturnType<TestSuiteNotebook["testCase"]>
| ReturnType<TestSuiteNotebook["assertThat"]>
| undefined = strategy == "testCase"
? methodResult as ReturnType<TestSuiteNotebook["testCase"]>
: (strategy == "assertThat"
? methodResult as ReturnType<TestSuiteNotebook["assertThat"]>
: undefined);
return {
notebook: c.source.instance,
tcName: String(c.callable),
friendlyName: String(c.callable).replace(/_test$/, ""),
assertion,
isValid: typeof assertion === "object" && assertion["isAssertion"],
name: String(c.callable),
methodResult,
strategy,
body,
};
},
),
);

const valid = testCases.filter((tc) => tc.isValid);
const valid = testCases.filter((tc) => tc.strategy != "invalid");
const totalCases = valid.reduce(
(total, tc) => total + tc.assertion.cases.length,
(total, tc) => total + (tc.body ? tc.body.casesCount() : 1),
0,
);

const defaultNB = sources[0];
const viewName = defaultNB?.notebookName;
const tapResultColName = defaultNB?.tapResultColName;

// deno-fmt-ignore
return [
...arbitrarySqlStmts,
...testCases.filter((tc) => !tc.isValid).map(tc => `-- Test Case "${tc.tcName}" did not return an assertThat instance (is ${typeof tc.assertion} instead)`),
`CREATE VIEW "${sources[0]?.notebookName}" AS`,
...testCases.filter((tc) => tc.strategy == "invalid").map(tc => `-- Test Case "${tc.name}" did not return an assertThat or testCase instance (is ${typeof tc.body} instead)`),
`CREATE VIEW "${viewName}" AS`,
` WITH`,
` tap_version AS (SELECT 'TAP version 14' AS tap_result),`,
` tap_plan AS (SELECT '1..${totalCases}' AS tap_result),`,
` ${valid.map((tc, index) => tc.assertion.SQL(tc.tcName, index + 1))}`,
` SELECT tap_result FROM tap_version`,
` tap_version AS (SELECT 'TAP version 14' AS ${tapResultColName}),`,
` tap_plan AS (SELECT '1..${totalCases}' AS ${tapResultColName}),`,
` ${valid.map((tc, index) => tc.body!.SQL(tc.name, index + 1))}`,
` SELECT ${tapResultColName} FROM tap_version`,
` UNION ALL`,
` SELECT tap_result FROM tap_plan`,
` SELECT ${tapResultColName} FROM tap_plan`,
` UNION ALL`,
` ${valid.map((tc) => `SELECT tap_result FROM "${tc.tcName}"`).join("\n UNION ALL\n")};`,
` ${valid.map((tc) => `SELECT ${tapResultColName} FROM "${tc.name}"`).join("\n UNION ALL\n")};`,
];
}
}

0 comments on commit 071c77d

Please sign in to comment.