import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import * as path from "@std/path"; import { hasExecutableCode } from "./executable-source.ts"; import { parseLcov } from "./coverage-metrics.ts"; /** A fixture module, and whether its content compiles to code that can run. */ interface Fixture { name: string; content: string; executable: boolean; } /** * Fixtures for the coverage pass. The augmentation holds an import whose * binding is used only in a type position: both compilers drop the import, so * nothing of that file survives to run. */ const COVERAGE_FIXTURES: Fixture[] = [ { name: "declarations.ts", content: [ "export interface Shape {", " sides: number;", "}", "export type Either = Shape | number;", ].join("\n"), executable: false, }, { name: "augmentation.ts", content: [ 'import { used } from "./values.ts";', "declare global {", " interface Window {", " extra?: typeof used;", " }", "}", "export {};", ].join("\n"), executable: false, }, { name: "values.ts", content: [ "export function used() {", " return 1;", "}", "export function skipped() {", " return 2;", "}", ].join("\n"), executable: true, }, { name: "barrel.ts", content: 'export * from "./values.ts";\n', executable: true, }, ]; describe("executable-source", () => { describe("hasExecutableCode()", () => { const declarationOnly: [string, string][] = [ [ "interfaces and type aliases", [ "export interface Shape {", " sides: number;", "}", "export type Either = Shape | number;", ].join("\n"), ], [ "ambient declarations", [ "declare const external: number;", "export declare function helper(x: number): number;", "export type Reference = typeof external;", ].join("\n"), ], [ "a global augmentation", [ "declare global {", " interface Window {", " extra?: number;", " }", "}", "export {};", ].join("\n"), ], [ "a namespace holding only types", [ "export namespace Shapes {", " export interface Circle {", " radius: number;", " }", "}", ].join("\n"), ], [ "a type-only re-export", 'export type { Shape } from "./shapes.ts";', ], [ "an import used only in type positions", [ 'import { Widget } from "./widget.ts";', "export type Rendered = (widget: Widget) => string;", ].join("\n"), ], [ "comments and the module marker", ["/** Everything here is commented out. */", "export {};"].join("\n"), ], [ "declarations in a file with no import or export", ["interface Local {", " a: number;", "}", "type Other = Local;"].join( "\n", ), ], ]; for (const [subject, content] of declarationOnly) { it(`returns \`false\` for ${subject}`, () => { expect(hasExecutableCode(content, "example.ts")).toBe(false); }); } const executable: [string, string][] = [ ["a value", "export const answer = 42;"], ["an enum", "export enum Color {\n Red,\n Green,\n}"], ["a const enum", "export const enum Speed {\n Slow = 1,\n}"], [ "a namespace holding a value", "export namespace Values {\n export const one = 1;\n}", ], [ "an abstract class", "export abstract class Base {\n abstract run(): void;\n}", ], [ "an import kept for its side effects", [ 'import "./register.ts";', "export interface Marker {", " m: true;", "}", ].join("\n"), ], ["a re-export", 'export * from "./values.ts";'], // Error recovery emits what the compiler managed to read, so a file it // cannot parse carries its full charge. ["a file that does not parse", "export const broken = {"], ["text that is not code at all", "This file is prose, not TypeScript.\n"], ]; for (const [subject, content] of executable) { it(`returns \`true\` for ${subject}`, () => { expect(hasExecutableCode(content, "example.ts")).toBe(true); }); } it("returns `true` for markup in a `.tsx` file", () => { const markup = "export const Greeting = () =>
hello
;"; expect(hasExecutableCode(markup, "component.tsx")).toBe(true); }); it("returns `false` for a `.tsx` file holding only types", () => { const types = [ "export interface Props {", " title: string;", "}", "export type Renderer = (props: Props) => unknown;", ].join("\n"); expect(hasExecutableCode(types, "component.tsx")).toBe(false); }); it("returns `true` for JavaScript that declares a value", () => { expect(hasExecutableCode("export const value = 1;\n", "mod.js")) .toBe(true); }); it("returns `false` for JavaScript that is all comment", () => { expect(hasExecutableCode("// nothing here\n", "mod.js")).toBe(false); }); describe("against a real coverage run", () => { // The coverage gate rests on a claim about Deno: a file this function // calls non-executable has no line Deno's coverage can report as // uncovered, so charging it nothing loses nothing. Deno transpiles with // swc and this function asks the TypeScript compiler, so a real coverage // pass over fixture modules holds the two to the same answer. // Resolved, because the report names the real path and a temporary // directory can be reached through a symlink (macOS puts one in front of // it). let root: string; let coverage: ReturnType; const recordFor = (name: string) => coverage.get(path.normalize(path.join(root, name))); const runInFixture = async (args: string[]) => { const result = await new Deno.Command(Deno.execPath(), { args, // Both runs read the same emit cache, which Deno keys on the config // it resolves; running them from the fixture directory keeps that the // same for the profile and the report. cwd: root, env: { DENO_COVERAGE_DIR: path.join(root, "raw") }, stdout: "null", stderr: "piped", }).output(); if (!result.success) { throw new Error( `\`deno ${args[0]}\` failed in the fixture directory: ${ new TextDecoder().decode(result.stderr) }`, ); } }; beforeAll(async () => { root = await Deno.realPath( await Deno.makeTempDir({ prefix: "executable-source-" }), ); for (const fixture of COVERAGE_FIXTURES) { await Deno.writeTextFile( path.join(root, fixture.name), fixture.content, ); } await Deno.writeTextFile( path.join(root, "fixture.test.ts"), [ // Side-effect imports, so every fixture is loaded whether or not it // binds anything. 'import "./declarations.ts";', 'import "./augmentation.ts";', 'import { used } from "./barrel.ts";', 'Deno.test("drive", () => {', ' if (used() !== 1) throw new Error("wrong");', "});", ].join("\n"), ); const rawDir = path.join(root, "raw"); await runInFixture([ "test", "--no-check", "--no-lock", `--coverage=${rawDir}`, "fixture.test.ts", ]); const lcovPath = path.join(root, "fixture.lcov"); await runInFixture([ "coverage", "--lcov", `--output=${lcovPath}`, rawDir, ]); coverage = parseLcov(await Deno.readTextFile(lcovPath)); }); afterAll(async () => { await Deno.remove(root, { recursive: true }); }); it("reports uncovered lines for a fixture holding code no test ran", () => { const values = recordFor("values.ts"); expect(values).toBeDefined(); expect([...values!.lineHits.values()].filter((hits) => hits === 0)) .not.toEqual([]); }); for (const fixture of COVERAGE_FIXTURES) { it(`classifies \`${fixture.name}\` as the report does`, () => { expect( hasExecutableCode(fixture.content, path.join(root, fixture.name)), ).toBe(fixture.executable); const record = recordFor(fixture.name); if (fixture.executable) { expect(record).toBeDefined(); return; } // Either no record at all, or one whose lines the module's own load // hit. A comment surviving into the emitted output can leave Deno a // line to report, and loading the module hits it. const hits = [...record?.lineHits.values() ?? []]; expect(hits.filter((count) => count === 0)).toEqual([]); }); } }); }); });