diff --git a/lib/analyze-action-post.js b/lib/analyze-action-post.js index 6986e029ba..b753e030ec 100644 --- a/lib/analyze-action-post.js +++ b/lib/analyze-action-post.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/analyze-action.js b/lib/analyze-action.js index af669490f9..5a99e1d1fa 100644 --- a/lib/analyze-action.js +++ b/lib/analyze-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/autobuild-action.js b/lib/autobuild-action.js index 6df04ea01d..6a59988733 100644 --- a/lib/autobuild-action.js +++ b/lib/autobuild-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/init-action-post.js b/lib/init-action-post.js index 67b6f43e88..a242527008 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/init-action.js b/lib/init-action.js index 6b1f265a55..cc32ddc52b 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/resolve-environment-action.js b/lib/resolve-environment-action.js index 19d6f4bc09..1631f7c2cb 100644 --- a/lib/resolve-environment-action.js +++ b/lib/resolve-environment-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/setup-codeql-action.js b/lib/setup-codeql-action.js index 24d4fe61b9..6f1f3261f3 100644 --- a/lib/setup-codeql-action.js +++ b/lib/setup-codeql-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/start-proxy-action-post.js b/lib/start-proxy-action-post.js index cb16ce9fef..d1b828de83 100644 --- a/lib/start-proxy-action-post.js +++ b/lib/start-proxy-action-post.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/start-proxy-action.js b/lib/start-proxy-action.js index 461b36194c..0dbbe3691a 100644 --- a/lib/start-proxy-action.js +++ b/lib/start-proxy-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/upload-lib.js b/lib/upload-lib.js index 16ba1a3039..b3d35b747c 100644 --- a/lib/upload-lib.js +++ b/lib/upload-lib.js @@ -47292,7 +47292,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/upload-sarif-action-post.js b/lib/upload-sarif-action-post.js index 87163ccb42..b3802f7331 100644 --- a/lib/upload-sarif-action-post.js +++ b/lib/upload-sarif-action-post.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/lib/upload-sarif-action.js b/lib/upload-sarif-action.js index b37c9a6a4f..c41ee0c1aa 100644 --- a/lib/upload-sarif-action.js +++ b/lib/upload-sarif-action.js @@ -45995,7 +45995,7 @@ var require_package = __commonJS({ lint: "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - ava: "npm run transpile && ava --serial --verbose", + ava: "npm run transpile && ava --verbose", test: "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", transpile: "tsc --build --verbose" diff --git a/package.json b/package.json index c02b9e0dac..197f910f05 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "eslint --report-unused-disable-directives --max-warnings=0 .", "lint-ci": "SARIF_ESLINT_IGNORE_SUPPRESSED=true eslint --report-unused-disable-directives --max-warnings=0 . --format @microsoft/eslint-formatter-sarif --output-file=eslint.sarif", "lint-fix": "eslint --report-unused-disable-directives --max-warnings=0 . --fix", - "ava": "npm run transpile && ava --serial --verbose", + "ava": "npm run transpile && ava --verbose", "test": "npm run ava -- src/", "test-debug": "npm run test -- --timeout=20m", "transpile": "tsc --build --verbose" diff --git a/src/actions-util.test.ts b/src/actions-util.test.ts index 68b5c63198..3940cf7551 100644 --- a/src/actions-util.test.ts +++ b/src/actions-util.test.ts @@ -100,7 +100,7 @@ test("computeAutomationID()", async (t) => { ); }); -test("getPullRequestBranches() with pull request context", (t) => { +test.serial("getPullRequestBranches() with pull request context", (t) => { withMockedContext( { pull_request: { @@ -119,89 +119,104 @@ test("getPullRequestBranches() with pull request context", (t) => { ); }); -test("getPullRequestBranches() returns undefined with push context", (t) => { - withMockedContext( - { - push: { - ref: "refs/heads/main", - }, - }, - () => { - t.is(getPullRequestBranches(), undefined); - t.is(isAnalyzingPullRequest(), false); - }, - ); -}); - -test("getPullRequestBranches() with Default Setup environment variables", (t) => { - withMockedContext({}, () => { - withMockedEnv( +test.serial( + "getPullRequestBranches() returns undefined with push context", + (t) => { + withMockedContext( { - CODE_SCANNING_REF: "refs/heads/feature-branch", - CODE_SCANNING_BASE_BRANCH: "main", - }, - () => { - t.deepEqual(getPullRequestBranches(), { - base: "main", - head: "refs/heads/feature-branch", - }); - t.is(isAnalyzingPullRequest(), true); - }, - ); - }); -}); - -test("getPullRequestBranches() returns undefined when only CODE_SCANNING_REF is set", (t) => { - withMockedContext({}, () => { - withMockedEnv( - { - CODE_SCANNING_REF: "refs/heads/feature-branch", - CODE_SCANNING_BASE_BRANCH: undefined, + push: { + ref: "refs/heads/main", + }, }, () => { t.is(getPullRequestBranches(), undefined); t.is(isAnalyzingPullRequest(), false); }, ); - }); -}); + }, +); -test("getPullRequestBranches() returns undefined when only CODE_SCANNING_BASE_BRANCH is set", (t) => { - withMockedContext({}, () => { - withMockedEnv( - { - CODE_SCANNING_REF: undefined, - CODE_SCANNING_BASE_BRANCH: "main", - }, - () => { - t.is(getPullRequestBranches(), undefined); - t.is(isAnalyzingPullRequest(), false); - }, - ); - }); -}); +test.serial( + "getPullRequestBranches() with Default Setup environment variables", + (t) => { + withMockedContext({}, () => { + withMockedEnv( + { + CODE_SCANNING_REF: "refs/heads/feature-branch", + CODE_SCANNING_BASE_BRANCH: "main", + }, + () => { + t.deepEqual(getPullRequestBranches(), { + base: "main", + head: "refs/heads/feature-branch", + }); + t.is(isAnalyzingPullRequest(), true); + }, + ); + }); + }, +); -test("getPullRequestBranches() returns undefined when no PR context", (t) => { - withMockedContext({}, () => { - withMockedEnv( - { - CODE_SCANNING_REF: undefined, - CODE_SCANNING_BASE_BRANCH: undefined, - }, - () => { - t.is(getPullRequestBranches(), undefined); - t.is(isAnalyzingPullRequest(), false); - }, - ); - }); -}); +test.serial( + "getPullRequestBranches() returns undefined when only CODE_SCANNING_REF is set", + (t) => { + withMockedContext({}, () => { + withMockedEnv( + { + CODE_SCANNING_REF: "refs/heads/feature-branch", + CODE_SCANNING_BASE_BRANCH: undefined, + }, + () => { + t.is(getPullRequestBranches(), undefined); + t.is(isAnalyzingPullRequest(), false); + }, + ); + }); + }, +); + +test.serial( + "getPullRequestBranches() returns undefined when only CODE_SCANNING_BASE_BRANCH is set", + (t) => { + withMockedContext({}, () => { + withMockedEnv( + { + CODE_SCANNING_REF: undefined, + CODE_SCANNING_BASE_BRANCH: "main", + }, + () => { + t.is(getPullRequestBranches(), undefined); + t.is(isAnalyzingPullRequest(), false); + }, + ); + }); + }, +); + +test.serial( + "getPullRequestBranches() returns undefined when no PR context", + (t) => { + withMockedContext({}, () => { + withMockedEnv( + { + CODE_SCANNING_REF: undefined, + CODE_SCANNING_BASE_BRANCH: undefined, + }, + () => { + t.is(getPullRequestBranches(), undefined); + t.is(isAnalyzingPullRequest(), false); + }, + ); + }); + }, +); -test("initializeEnvironment", (t) => { +test.serial("initializeEnvironment", (t) => { initializeEnvironment("1.2.3"); t.deepEqual(process.env[EnvVar.VERSION], "1.2.3"); }); -test("fixCodeQualityCategory", (t) => { +test.serial("fixCodeQualityCategory", (t) => { withMockedEnv( { GITHUB_EVENT_NAME: "dynamic", @@ -249,14 +264,17 @@ test("fixCodeQualityCategory", (t) => { ); }); -test("isDynamicWorkflow() returns true if event name is `dynamic`", (t) => { - process.env.GITHUB_EVENT_NAME = "dynamic"; - t.assert(isDynamicWorkflow()); - process.env.GITHUB_EVENT_NAME = "push"; - t.false(isDynamicWorkflow()); -}); +test.serial( + "isDynamicWorkflow() returns true if event name is `dynamic`", + (t) => { + process.env.GITHUB_EVENT_NAME = "dynamic"; + t.assert(isDynamicWorkflow()); + process.env.GITHUB_EVENT_NAME = "push"; + t.false(isDynamicWorkflow()); + }, +); -test("isDefaultSetup() returns true when expected", (t) => { +test.serial("isDefaultSetup() returns true when expected", (t) => { process.env.GITHUB_EVENT_NAME = "dynamic"; process.env[EnvVar.ANALYSIS_KEY] = "dynamic/github-code-scanning"; t.assert(isDefaultSetup()); diff --git a/src/analyses.test.ts b/src/analyses.test.ts index 36d3d316fe..293b4be6d2 100644 --- a/src/analyses.test.ts +++ b/src/analyses.test.ts @@ -50,31 +50,40 @@ test("Parsing analysis kinds requires at least one analysis kind", async (t) => }); }); -test("getAnalysisKinds - returns expected analysis kinds for `analysis-kinds` input", async (t) => { - const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); - requiredInputStub - .withArgs("analysis-kinds") - .returns("code-scanning,code-quality"); - const result = await getAnalysisKinds(getRunnerLogger(true), true); - t.assert(result.includes(AnalysisKind.CodeScanning)); - t.assert(result.includes(AnalysisKind.CodeQuality)); -}); - -test("getAnalysisKinds - includes `code-quality` when deprecated `quality-queries` input is used", async (t) => { - const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); - requiredInputStub.withArgs("analysis-kinds").returns("code-scanning"); - const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput"); - optionalInputStub.withArgs("quality-queries").returns("code-quality"); - const result = await getAnalysisKinds(getRunnerLogger(true), true); - t.assert(result.includes(AnalysisKind.CodeScanning)); - t.assert(result.includes(AnalysisKind.CodeQuality)); -}); - -test("getAnalysisKinds - throws if `analysis-kinds` input is invalid", async (t) => { - const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); - requiredInputStub.withArgs("analysis-kinds").returns("no-such-thing"); - await t.throwsAsync(getAnalysisKinds(getRunnerLogger(true), true)); -}); +test.serial( + "getAnalysisKinds - returns expected analysis kinds for `analysis-kinds` input", + async (t) => { + const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); + requiredInputStub + .withArgs("analysis-kinds") + .returns("code-scanning,code-quality"); + const result = await getAnalysisKinds(getRunnerLogger(true), true); + t.assert(result.includes(AnalysisKind.CodeScanning)); + t.assert(result.includes(AnalysisKind.CodeQuality)); + }, +); + +test.serial( + "getAnalysisKinds - includes `code-quality` when deprecated `quality-queries` input is used", + async (t) => { + const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); + requiredInputStub.withArgs("analysis-kinds").returns("code-scanning"); + const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput"); + optionalInputStub.withArgs("quality-queries").returns("code-quality"); + const result = await getAnalysisKinds(getRunnerLogger(true), true); + t.assert(result.includes(AnalysisKind.CodeScanning)); + t.assert(result.includes(AnalysisKind.CodeQuality)); + }, +); + +test.serial( + "getAnalysisKinds - throws if `analysis-kinds` input is invalid", + async (t) => { + const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); + requiredInputStub.withArgs("analysis-kinds").returns("no-such-thing"); + await t.throwsAsync(getAnalysisKinds(getRunnerLogger(true), true)); + }, +); // Test the compatibility matrix by looping through all analysis kinds. const analysisKinds = Object.values(AnalysisKind); @@ -86,25 +95,31 @@ for (let i = 0; i < analysisKinds.length; i++) { if (analysisKind === otherAnalysis) continue; if (compatibilityMatrix[analysisKind].has(otherAnalysis)) { - test(`getAnalysisKinds - allows ${analysisKind} with ${otherAnalysis}`, async (t) => { - const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); - requiredInputStub - .withArgs("analysis-kinds") - .returns([analysisKind, otherAnalysis].join(",")); - const result = await getAnalysisKinds(getRunnerLogger(true), true); - t.is(result.length, 2); - }); + test.serial( + `getAnalysisKinds - allows ${analysisKind} with ${otherAnalysis}`, + async (t) => { + const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); + requiredInputStub + .withArgs("analysis-kinds") + .returns([analysisKind, otherAnalysis].join(",")); + const result = await getAnalysisKinds(getRunnerLogger(true), true); + t.is(result.length, 2); + }, + ); } else { - test(`getAnalysisKinds - throws if ${analysisKind} is enabled with ${otherAnalysis}`, async (t) => { - const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); - requiredInputStub - .withArgs("analysis-kinds") - .returns([analysisKind, otherAnalysis].join(",")); - await t.throwsAsync(getAnalysisKinds(getRunnerLogger(true), true), { - instanceOf: ConfigurationError, - message: `${analysisKind} and ${otherAnalysis} cannot be enabled at the same time`, - }); - }); + test.serial( + `getAnalysisKinds - throws if ${analysisKind} is enabled with ${otherAnalysis}`, + async (t) => { + const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput"); + requiredInputStub + .withArgs("analysis-kinds") + .returns([analysisKind, otherAnalysis].join(",")); + await t.throwsAsync(getAnalysisKinds(getRunnerLogger(true), true), { + instanceOf: ConfigurationError, + message: `${analysisKind} and ${otherAnalysis} cannot be enabled at the same time`, + }); + }, + ); } } } @@ -122,44 +137,50 @@ test("Code Scanning configuration does not accept other SARIF extensions", (t) = } }); -test("Risk Assessment configuration transforms SARIF upload payload", (t) => { - process.env[EnvVar.RISK_ASSESSMENT_ID] = "1"; - const payload = RiskAssessment.transformPayload({ - commit_oid: "abc", - sarif: "sarif", - ref: "ref", - workflow_run_attempt: 1, - workflow_run_id: 1, - checkout_uri: "uri", - tool_names: [], - }) as AssessmentPayload; - - const expected: AssessmentPayload = { sarif: "sarif", assessment_id: 1 }; - t.deepEqual(expected, payload); -}); - -test("Risk Assessment configuration throws for negative assessment IDs", (t) => { - process.env[EnvVar.RISK_ASSESSMENT_ID] = "-1"; - t.throws( - () => - RiskAssessment.transformPayload({ - commit_oid: "abc", - sarif: "sarif", - ref: "ref", - workflow_run_attempt: 1, - workflow_run_id: 1, - checkout_uri: "uri", - tool_names: [], - }), - { - instanceOf: Error, - message: (msg) => - msg.startsWith(`${EnvVar.RISK_ASSESSMENT_ID} must not be negative: `), - }, - ); -}); - -test("Risk Assessment configuration throws for invalid IDs", (t) => { +test.serial( + "Risk Assessment configuration transforms SARIF upload payload", + (t) => { + process.env[EnvVar.RISK_ASSESSMENT_ID] = "1"; + const payload = RiskAssessment.transformPayload({ + commit_oid: "abc", + sarif: "sarif", + ref: "ref", + workflow_run_attempt: 1, + workflow_run_id: 1, + checkout_uri: "uri", + tool_names: [], + }) as AssessmentPayload; + + const expected: AssessmentPayload = { sarif: "sarif", assessment_id: 1 }; + t.deepEqual(expected, payload); + }, +); + +test.serial( + "Risk Assessment configuration throws for negative assessment IDs", + (t) => { + process.env[EnvVar.RISK_ASSESSMENT_ID] = "-1"; + t.throws( + () => + RiskAssessment.transformPayload({ + commit_oid: "abc", + sarif: "sarif", + ref: "ref", + workflow_run_attempt: 1, + workflow_run_id: 1, + checkout_uri: "uri", + tool_names: [], + }), + { + instanceOf: Error, + message: (msg) => + msg.startsWith(`${EnvVar.RISK_ASSESSMENT_ID} must not be negative: `), + }, + ); + }, +); + +test.serial("Risk Assessment configuration throws for invalid IDs", (t) => { process.env[EnvVar.RISK_ASSESSMENT_ID] = "foo"; t.throws( () => diff --git a/src/analyze.test.ts b/src/analyze.test.ts index a5ab7a34d7..664c238535 100644 --- a/src/analyze.test.ts +++ b/src/analyze.test.ts @@ -32,7 +32,7 @@ setupTests(test); * - Checks that the duration fields are populated for the correct language. * - Checks that the QA telemetry status report fields are populated when the QA feature flag is enabled. */ -test("status report fields", async (t) => { +test.serial("status report fields", async (t) => { return await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); diff --git a/src/api-client.test.ts b/src/api-client.test.ts index 3af9ae282f..d0311d0dc5 100644 --- a/src/api-client.test.ts +++ b/src/api-client.test.ts @@ -14,7 +14,7 @@ test.beforeEach(() => { util.initializeEnvironment(actionsUtil.getActionVersion()); }); -test("getApiClient", async (t) => { +test.serial("getApiClient", async (t) => { const pluginStub: sinon.SinonStub = sinon.stub(githubUtils.GitHub, "plugin"); const githubStub: sinon.SinonStub = sinon.stub(); pluginStub.returns(githubStub); @@ -61,7 +61,7 @@ function mockGetMetaVersionHeader( return spyGetContents; } -test("getGitHubVersion for Dotcom", async (t) => { +test.serial("getGitHubVersion for Dotcom", async (t) => { const apiDetails = { auth: "", url: "https://github.com", @@ -75,7 +75,7 @@ test("getGitHubVersion for Dotcom", async (t) => { t.deepEqual(util.GitHubVariant.DOTCOM, v.type); }); -test("getGitHubVersion for GHES", async (t) => { +test.serial("getGitHubVersion for GHES", async (t) => { mockGetMetaVersionHeader("2.0"); const v2 = await api.getGitHubVersionFromApi(api.getApiClient(), { auth: "", @@ -88,7 +88,7 @@ test("getGitHubVersion for GHES", async (t) => { ); }); -test("getGitHubVersion for different domain", async (t) => { +test.serial("getGitHubVersion for different domain", async (t) => { mockGetMetaVersionHeader(undefined); const v3 = await api.getGitHubVersionFromApi(api.getApiClient(), { auth: "", @@ -98,7 +98,7 @@ test("getGitHubVersion for different domain", async (t) => { t.deepEqual({ type: util.GitHubVariant.DOTCOM }, v3); }); -test("getGitHubVersion for GHEC-DR", async (t) => { +test.serial("getGitHubVersion for GHEC-DR", async (t) => { mockGetMetaVersionHeader("ghe.com"); const gheDotcom = await api.getGitHubVersionFromApi(api.getApiClient(), { auth: "", @@ -108,96 +108,99 @@ test("getGitHubVersion for GHEC-DR", async (t) => { t.deepEqual({ type: util.GitHubVariant.GHEC_DR }, gheDotcom); }); -test("wrapApiConfigurationError correctly wraps specific configuration errors", (t) => { - // We don't reclassify arbitrary errors - const arbitraryError = new Error("arbitrary error"); - let res = api.wrapApiConfigurationError(arbitraryError); - t.is(res, arbitraryError); - - // Same goes for arbitrary errors - const configError = new util.ConfigurationError("arbitrary error"); - res = api.wrapApiConfigurationError(configError); - t.is(res, configError); - - // If an HTTP error doesn't contain a specific error message, we don't - // wrap is an an API error. - const httpError = new util.HTTPError("arbitrary HTTP error", 456); - res = api.wrapApiConfigurationError(httpError); - t.is(res, httpError); - - // For other HTTP errors, we wrap them as Configuration errors if they contain - // specific error messages. - const httpNotFoundError = new util.HTTPError("commit not found", 404); - res = api.wrapApiConfigurationError(httpNotFoundError); - t.deepEqual(res, new util.ConfigurationError("commit not found")); - - const refNotFoundError = new util.HTTPError( - "ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest", - 404, - ); - res = api.wrapApiConfigurationError(refNotFoundError); - t.deepEqual( - res, - new util.ConfigurationError( +test.serial( + "wrapApiConfigurationError correctly wraps specific configuration errors", + (t) => { + // We don't reclassify arbitrary errors + const arbitraryError = new Error("arbitrary error"); + let res = api.wrapApiConfigurationError(arbitraryError); + t.is(res, arbitraryError); + + // Same goes for arbitrary errors + const configError = new util.ConfigurationError("arbitrary error"); + res = api.wrapApiConfigurationError(configError); + t.is(res, configError); + + // If an HTTP error doesn't contain a specific error message, we don't + // wrap is an an API error. + const httpError = new util.HTTPError("arbitrary HTTP error", 456); + res = api.wrapApiConfigurationError(httpError); + t.is(res, httpError); + + // For other HTTP errors, we wrap them as Configuration errors if they contain + // specific error messages. + const httpNotFoundError = new util.HTTPError("commit not found", 404); + res = api.wrapApiConfigurationError(httpNotFoundError); + t.deepEqual(res, new util.ConfigurationError("commit not found")); + + const refNotFoundError = new util.HTTPError( "ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest", - ), - ); - - const apiRateLimitError = new util.HTTPError( - "API rate limit exceeded for installation", - 403, - ); - res = api.wrapApiConfigurationError(apiRateLimitError); - t.deepEqual( - res, - new util.ConfigurationError("API rate limit exceeded for installation"), - ); - - const tokenSuggestionMessage = - "Please check that your token is valid and has the required permissions: contents: read, security-events: write"; - const badCredentialsError = new util.HTTPError("Bad credentials", 401); - res = api.wrapApiConfigurationError(badCredentialsError); - t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); - - const notFoundError = new util.HTTPError("Not Found", 404); - res = api.wrapApiConfigurationError(notFoundError); - t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); - - const resourceNotAccessibleError = new util.HTTPError( - "Resource not accessible by integration", - 403, - ); - res = api.wrapApiConfigurationError(resourceNotAccessibleError); - t.deepEqual( - res, - new util.ConfigurationError("Resource not accessible by integration"), - ); - - // Enablement errors. - const enablementErrorMessages = [ - "Code Security must be enabled for this repository to use code scanning", - "Advanced Security must be enabled for this repository to use code scanning", - "Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.", - ]; - const transforms = [ - (msg: string) => msg, - (msg: string) => msg.toLowerCase(), - (msg: string) => msg.toLocaleUpperCase(), - ]; - - for (const enablementErrorMessage of enablementErrorMessages) { - for (const transform of transforms) { - const enablementError = new util.HTTPError( - transform(enablementErrorMessage), - 403, - ); - res = api.wrapApiConfigurationError(enablementError); - t.deepEqual( - res, - new util.ConfigurationError( - api.getFeatureEnablementError(enablementError.message), - ), - ); + 404, + ); + res = api.wrapApiConfigurationError(refNotFoundError); + t.deepEqual( + res, + new util.ConfigurationError( + "ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest", + ), + ); + + const apiRateLimitError = new util.HTTPError( + "API rate limit exceeded for installation", + 403, + ); + res = api.wrapApiConfigurationError(apiRateLimitError); + t.deepEqual( + res, + new util.ConfigurationError("API rate limit exceeded for installation"), + ); + + const tokenSuggestionMessage = + "Please check that your token is valid and has the required permissions: contents: read, security-events: write"; + const badCredentialsError = new util.HTTPError("Bad credentials", 401); + res = api.wrapApiConfigurationError(badCredentialsError); + t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); + + const notFoundError = new util.HTTPError("Not Found", 404); + res = api.wrapApiConfigurationError(notFoundError); + t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); + + const resourceNotAccessibleError = new util.HTTPError( + "Resource not accessible by integration", + 403, + ); + res = api.wrapApiConfigurationError(resourceNotAccessibleError); + t.deepEqual( + res, + new util.ConfigurationError("Resource not accessible by integration"), + ); + + // Enablement errors. + const enablementErrorMessages = [ + "Code Security must be enabled for this repository to use code scanning", + "Advanced Security must be enabled for this repository to use code scanning", + "Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.", + ]; + const transforms = [ + (msg: string) => msg, + (msg: string) => msg.toLowerCase(), + (msg: string) => msg.toLocaleUpperCase(), + ]; + + for (const enablementErrorMessage of enablementErrorMessages) { + for (const transform of transforms) { + const enablementError = new util.HTTPError( + transform(enablementErrorMessage), + 403, + ); + res = api.wrapApiConfigurationError(enablementError); + t.deepEqual( + res, + new util.ConfigurationError( + api.getFeatureEnablementError(enablementError.message), + ), + ); + } } - } -}); + }, +); diff --git a/src/cli-errors.test.ts b/src/cli-errors.test.ts index 58ebfa2c4c..7a3ed892ba 100644 --- a/src/cli-errors.test.ts +++ b/src/cli-errors.test.ts @@ -131,27 +131,30 @@ for (const [platform, arch] of [ ["linux", "arm64"], ["win32", "arm64"], ]) { - test(`wrapCliConfigurationError - ${platform}/${arch} unsupported`, (t) => { - sinon.stub(process, "platform").value(platform); - sinon.stub(process, "arch").value(arch); - const commandError = new CommandInvocationError( - "codeql", - ["version"], - 1, - "Some error", - ); - const cliError = new CliError(commandError); - - const wrappedError = wrapCliConfigurationError(cliError); - - t.true(wrappedError instanceof ConfigurationError); - t.true( - wrappedError.message.includes( - "CodeQL CLI does not support the platform/architecture combination", - ), - ); - t.true(wrappedError.message.includes(`${platform}/${arch}`)); - }); + test.serial( + `wrapCliConfigurationError - ${platform}/${arch} unsupported`, + (t) => { + sinon.stub(process, "platform").value(platform); + sinon.stub(process, "arch").value(arch); + const commandError = new CommandInvocationError( + "codeql", + ["version"], + 1, + "Some error", + ); + const cliError = new CliError(commandError); + + const wrappedError = wrapCliConfigurationError(cliError); + + t.true(wrappedError instanceof ConfigurationError); + t.true( + wrappedError.message.includes( + "CodeQL CLI does not support the platform/architecture combination", + ), + ); + t.true(wrappedError.message.includes(`${platform}/${arch}`)); + }, + ); } test("wrapCliConfigurationError - supported platform", (t) => { diff --git a/src/codeql.test.ts b/src/codeql.test.ts index eb1ea9b346..cfbddf4f78 100644 --- a/src/codeql.test.ts +++ b/src/codeql.test.ts @@ -120,19 +120,53 @@ async function stubCodeql(): Promise { return codeqlObject; } -test("downloads and caches explicitly requested bundles that aren't in the toolcache", async (t) => { - const features = createFeatures([]); +test.serial( + "downloads and caches explicitly requested bundles that aren't in the toolcache", + async (t) => { + const features = createFeatures([]); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - const versions = ["20200601", "20200610"]; + const versions = ["20200601", "20200610"]; - for (let i = 0; i < versions.length; i++) { - const version = versions[i]; + for (let i = 0; i < versions.length; i++) { + const version = versions[i]; + const url = mockBundleDownloadApi({ + tagName: `codeql-bundle-${version}`, + isPinned: false, + }); + const result = await codeql.setupCodeQL( + url, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + getRunnerLogger(true), + false, + ); + + t.assert(toolcache.find("CodeQL", `0.0.0-${version}`)); + t.is(result.toolsVersion, `0.0.0-${version}`); + t.is(result.toolsSource, ToolsSource.Download); + } + + t.is(toolcache.findAllVersions("CodeQL").length, 2); + }); + }, +); + +test.serial( + "caches semantically versioned bundles using their semantic version number", + async (t) => { + const features = createFeatures([]); + + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); const url = mockBundleDownloadApi({ - tagName: `codeql-bundle-${version}`, + tagName: `codeql-bundle-v2.15.0`, isPinned: false, }); const result = await codeql.setupCodeQL( @@ -146,78 +180,53 @@ test("downloads and caches explicitly requested bundles that aren't in the toolc false, ); - t.assert(toolcache.find("CodeQL", `0.0.0-${version}`)); - t.is(result.toolsVersion, `0.0.0-${version}`); + t.is(toolcache.findAllVersions("CodeQL").length, 1); + t.assert(toolcache.find("CodeQL", `2.15.0`)); + t.is(result.toolsVersion, `2.15.0`); t.is(result.toolsSource, ToolsSource.Download); - } - - t.is(toolcache.findAllVersions("CodeQL").length, 2); - }); -}); - -test("caches semantically versioned bundles using their semantic version number", async (t) => { - const features = createFeatures([]); - - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const url = mockBundleDownloadApi({ - tagName: `codeql-bundle-v2.15.0`, - isPinned: false, + if (result.toolsDownloadStatusReport) { + assertDurationsInteger(t, result.toolsDownloadStatusReport); + } }); - const result = await codeql.setupCodeQL( - url, - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - getRunnerLogger(true), - false, - ); - - t.is(toolcache.findAllVersions("CodeQL").length, 1); - t.assert(toolcache.find("CodeQL", `2.15.0`)); - t.is(result.toolsVersion, `2.15.0`); - t.is(result.toolsSource, ToolsSource.Download); - if (result.toolsDownloadStatusReport) { - assertDurationsInteger(t, result.toolsDownloadStatusReport); - } - }); -}); + }, +); -test("downloads an explicitly requested bundle even if a different version is cached", async (t) => { - const features = createFeatures([]); +test.serial( + "downloads an explicitly requested bundle even if a different version is cached", + async (t) => { + const features = createFeatures([]); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - await installIntoToolcache({ - tagName: "codeql-bundle-20200601", - isPinned: true, - tmpDir, - }); + await installIntoToolcache({ + tagName: "codeql-bundle-20200601", + isPinned: true, + tmpDir, + }); - const url = mockBundleDownloadApi({ - tagName: "codeql-bundle-20200610", + const url = mockBundleDownloadApi({ + tagName: "codeql-bundle-20200610", + }); + const result = await codeql.setupCodeQL( + url, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + getRunnerLogger(true), + false, + ); + t.assert(toolcache.find("CodeQL", "0.0.0-20200610")); + t.deepEqual(result.toolsVersion, "0.0.0-20200610"); + t.is(result.toolsSource, ToolsSource.Download); + if (result.toolsDownloadStatusReport) { + assertDurationsInteger(t, result.toolsDownloadStatusReport); + } }); - const result = await codeql.setupCodeQL( - url, - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - getRunnerLogger(true), - false, - ); - t.assert(toolcache.find("CodeQL", "0.0.0-20200610")); - t.deepEqual(result.toolsVersion, "0.0.0-20200610"); - t.is(result.toolsSource, ToolsSource.Download); - if (result.toolsDownloadStatusReport) { - assertDurationsInteger(t, result.toolsDownloadStatusReport); - } - }); -}); + }, +); const EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES = [ { @@ -234,37 +243,42 @@ for (const { tagName, expectedToolcacheVersion, } of EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES) { - test(`caches explicitly requested bundle ${tagName} as ${expectedToolcacheVersion}`, async (t) => { - const features = createFeatures([]); + test.serial( + `caches explicitly requested bundle ${tagName} as ${expectedToolcacheVersion}`, + async (t) => { + const features = createFeatures([]); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - mockApiDetails(SAMPLE_DOTCOM_API_DETAILS); - sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + mockApiDetails(SAMPLE_DOTCOM_API_DETAILS); + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); - const url = mockBundleDownloadApi({ - tagName, - }); + const url = mockBundleDownloadApi({ + tagName, + }); - const result = await codeql.setupCodeQL( - url, - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - getRunnerLogger(true), - false, - ); - t.assert(toolcache.find("CodeQL", expectedToolcacheVersion)); - t.deepEqual(result.toolsVersion, expectedToolcacheVersion); - t.is(result.toolsSource, ToolsSource.Download); - t.assert( - Number.isInteger(result.toolsDownloadStatusReport?.downloadDurationMs), - ); - }); - }); + const result = await codeql.setupCodeQL( + url, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + getRunnerLogger(true), + false, + ); + t.assert(toolcache.find("CodeQL", expectedToolcacheVersion)); + t.deepEqual(result.toolsVersion, expectedToolcacheVersion); + t.is(result.toolsSource, ToolsSource.Download); + t.assert( + Number.isInteger( + result.toolsDownloadStatusReport?.downloadDurationMs, + ), + ); + }); + }, + ); } for (const toolcacheVersion of [ @@ -273,7 +287,7 @@ for (const toolcacheVersion of [ SAMPLE_DEFAULT_CLI_VERSION.cliVersion, `${SAMPLE_DEFAULT_CLI_VERSION.cliVersion}-20230101`, ]) { - test( + test.serial( `uses tools from toolcache when ${SAMPLE_DEFAULT_CLI_VERSION.cliVersion} is requested and ` + `${toolcacheVersion} is installed`, async (t) => { @@ -308,158 +322,170 @@ for (const toolcacheVersion of [ ); } -test(`uses a cached bundle when no tools input is given on GHES`, async (t) => { - const features = createFeatures([]); +test.serial( + `uses a cached bundle when no tools input is given on GHES`, + async (t) => { + const features = createFeatures([]); + + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await installIntoToolcache({ + tagName: "codeql-bundle-20200601", + isPinned: true, + tmpDir, + }); - await installIntoToolcache({ - tagName: "codeql-bundle-20200601", - isPinned: true, - tmpDir, + const result = await codeql.setupCodeQL( + undefined, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.GHES, + { + cliVersion: defaults.cliVersion, + tagName: defaults.bundleVersion, + }, + features, + getRunnerLogger(true), + false, + ); + t.deepEqual(result.toolsVersion, "0.0.0-20200601"); + t.is(result.toolsSource, ToolsSource.Toolcache); + t.is(result.toolsDownloadStatusReport?.combinedDurationMs, undefined); + t.is(result.toolsDownloadStatusReport?.downloadDurationMs, undefined); + t.is(result.toolsDownloadStatusReport?.extractionDurationMs, undefined); + + const cachedVersions = toolcache.findAllVersions("CodeQL"); + t.is(cachedVersions.length, 1); }); + }, +); - const result = await codeql.setupCodeQL( - undefined, - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.GHES, - { - cliVersion: defaults.cliVersion, - tagName: defaults.bundleVersion, - }, - features, - getRunnerLogger(true), - false, - ); - t.deepEqual(result.toolsVersion, "0.0.0-20200601"); - t.is(result.toolsSource, ToolsSource.Toolcache); - t.is(result.toolsDownloadStatusReport?.combinedDurationMs, undefined); - t.is(result.toolsDownloadStatusReport?.downloadDurationMs, undefined); - t.is(result.toolsDownloadStatusReport?.extractionDurationMs, undefined); - - const cachedVersions = toolcache.findAllVersions("CodeQL"); - t.is(cachedVersions.length, 1); - }); -}); +test.serial( + `downloads bundle if only an unpinned version is cached on GHES`, + async (t) => { + const features = createFeatures([]); -test(`downloads bundle if only an unpinned version is cached on GHES`, async (t) => { - const features = createFeatures([]); + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await installIntoToolcache({ + tagName: "codeql-bundle-20200601", + isPinned: false, + tmpDir, + }); - await installIntoToolcache({ - tagName: "codeql-bundle-20200601", - isPinned: false, - tmpDir, - }); + mockBundleDownloadApi({ + tagName: defaults.bundleVersion, + }); + const result = await codeql.setupCodeQL( + undefined, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.GHES, + { + cliVersion: defaults.cliVersion, + tagName: defaults.bundleVersion, + }, + features, + getRunnerLogger(true), + false, + ); + t.deepEqual(result.toolsVersion, defaults.cliVersion); + t.is(result.toolsSource, ToolsSource.Download); + if (result.toolsDownloadStatusReport) { + assertDurationsInteger(t, result.toolsDownloadStatusReport); + } - mockBundleDownloadApi({ - tagName: defaults.bundleVersion, + const cachedVersions = toolcache.findAllVersions("CodeQL"); + t.is(cachedVersions.length, 2); }); - const result = await codeql.setupCodeQL( - undefined, - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.GHES, - { - cliVersion: defaults.cliVersion, - tagName: defaults.bundleVersion, - }, - features, - getRunnerLogger(true), - false, - ); - t.deepEqual(result.toolsVersion, defaults.cliVersion); - t.is(result.toolsSource, ToolsSource.Download); - if (result.toolsDownloadStatusReport) { - assertDurationsInteger(t, result.toolsDownloadStatusReport); - } + }, +); - const cachedVersions = toolcache.findAllVersions("CodeQL"); - t.is(cachedVersions.length, 2); - }); -}); +test.serial( + 'downloads bundle if "latest" tools specified but not cached', + async (t) => { + const features = createFeatures([]); -test('downloads bundle if "latest" tools specified but not cached', async (t) => { - const features = createFeatures([]); + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await installIntoToolcache({ + tagName: "codeql-bundle-20200601", + isPinned: true, + tmpDir, + }); - await installIntoToolcache({ - tagName: "codeql-bundle-20200601", - isPinned: true, - tmpDir, - }); + mockBundleDownloadApi({ + tagName: defaults.bundleVersion, + }); + const result = await codeql.setupCodeQL( + "latest", + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + getRunnerLogger(true), + false, + ); + t.deepEqual(result.toolsVersion, defaults.cliVersion); + t.is(result.toolsSource, ToolsSource.Download); + if (result.toolsDownloadStatusReport) { + assertDurationsInteger(t, result.toolsDownloadStatusReport); + } - mockBundleDownloadApi({ - tagName: defaults.bundleVersion, + const cachedVersions = toolcache.findAllVersions("CodeQL"); + t.is(cachedVersions.length, 2); }); - const result = await codeql.setupCodeQL( - "latest", - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - getRunnerLogger(true), - false, - ); - t.deepEqual(result.toolsVersion, defaults.cliVersion); - t.is(result.toolsSource, ToolsSource.Download); - if (result.toolsDownloadStatusReport) { - assertDurationsInteger(t, result.toolsDownloadStatusReport); - } - - const cachedVersions = toolcache.findAllVersions("CodeQL"); - t.is(cachedVersions.length, 2); - }); -}); + }, +); -test("bundle URL from another repo is cached as 0.0.0-bundleVersion", async (t) => { - const features = createFeatures([]); +test.serial( + "bundle URL from another repo is cached as 0.0.0-bundleVersion", + async (t) => { + const features = createFeatures([]); - await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); - mockApiDetails(SAMPLE_DOTCOM_API_DETAILS); - sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); - const releasesApiMock = mockReleaseApi({ - assetNames: ["cli-version-2.14.6.txt"], - tagName: "codeql-bundle-20230203", - }); - mockBundleDownloadApi({ - repo: "codeql-testing/codeql-cli-nightlies", - platformSpecific: false, - tagName: "codeql-bundle-20230203", - }); - const result = await codeql.setupCodeQL( - "https://github.com/codeql-testing/codeql-cli-nightlies/releases/download/codeql-bundle-20230203/codeql-bundle.tar.gz", - SAMPLE_DOTCOM_API_DETAILS, - tmpDir, - util.GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - getRunnerLogger(true), - false, - ); + mockApiDetails(SAMPLE_DOTCOM_API_DETAILS); + sinon.stub(actionsUtil, "isRunningLocalAction").returns(true); + const releasesApiMock = mockReleaseApi({ + assetNames: ["cli-version-2.14.6.txt"], + tagName: "codeql-bundle-20230203", + }); + mockBundleDownloadApi({ + repo: "codeql-testing/codeql-cli-nightlies", + platformSpecific: false, + tagName: "codeql-bundle-20230203", + }); + const result = await codeql.setupCodeQL( + "https://github.com/codeql-testing/codeql-cli-nightlies/releases/download/codeql-bundle-20230203/codeql-bundle.tar.gz", + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + util.GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + getRunnerLogger(true), + false, + ); - t.is(result.toolsVersion, "0.0.0-20230203"); - t.is(result.toolsSource, ToolsSource.Download); - if (result.toolsDownloadStatusReport) { - assertDurationsInteger(t, result.toolsDownloadStatusReport); - } + t.is(result.toolsVersion, "0.0.0-20230203"); + t.is(result.toolsSource, ToolsSource.Download); + if (result.toolsDownloadStatusReport) { + assertDurationsInteger(t, result.toolsDownloadStatusReport); + } - const cachedVersions = toolcache.findAllVersions("CodeQL"); - t.is(cachedVersions.length, 1); - t.is(cachedVersions[0], "0.0.0-20230203"); + const cachedVersions = toolcache.findAllVersions("CodeQL"); + t.is(cachedVersions.length, 1); + t.is(cachedVersions[0], "0.0.0-20230203"); - t.false(releasesApiMock.isDone()); - }); -}); + t.false(releasesApiMock.isDone()); + }); + }, +); function assertDurationsInteger( t: ExecutionContext, @@ -472,7 +498,7 @@ function assertDurationsInteger( } } -test("getExtraOptions works for explicit paths", (t) => { +test.serial("getExtraOptions works for explicit paths", (t) => { t.deepEqual(codeql.getExtraOptions({}, ["foo"], []), []); t.deepEqual(codeql.getExtraOptions({ foo: [42] }, ["foo"], []), ["42"]); @@ -483,11 +509,11 @@ test("getExtraOptions works for explicit paths", (t) => { ); }); -test("getExtraOptions works for wildcards", (t) => { +test.serial("getExtraOptions works for wildcards", (t) => { t.deepEqual(codeql.getExtraOptions({ "*": [42] }, ["foo"], []), ["42"]); }); -test("getExtraOptions works for wildcards and explicit paths", (t) => { +test.serial("getExtraOptions works for wildcards and explicit paths", (t) => { const o1 = { "*": [42], foo: [87] }; t.deepEqual(codeql.getExtraOptions(o1, ["foo"], []), ["42", "87"]); @@ -499,7 +525,7 @@ test("getExtraOptions works for wildcards and explicit paths", (t) => { t.deepEqual(codeql.getExtraOptions(o3, p, []), ["42", "87", "99"]); }); -test("getExtraOptions throws for bad content", (t) => { +test.serial("getExtraOptions throws for bad content", (t) => { t.throws(() => codeql.getExtraOptions({ "*": 42 }, ["foo"], [])); t.throws(() => codeql.getExtraOptions({ foo: 87 }, ["foo"], [])); @@ -564,7 +590,7 @@ const injectedConfigMacro = test.macro({ `databaseInitCluster() injected config: ${providedTitle}`, }); -test( +test.serial( "basic", injectedConfigMacro, { @@ -574,7 +600,7 @@ test( {}, ); -test( +test.serial( "injected packs from input", injectedConfigMacro, { @@ -587,7 +613,7 @@ test( }, ); -test( +test.serial( "injected packs from input with existing packs combines", injectedConfigMacro, { @@ -609,7 +635,7 @@ test( }, ); -test( +test.serial( "injected packs from input with existing packs overrides", injectedConfigMacro, { @@ -629,7 +655,7 @@ test( ); // similar, but with queries -test( +test.serial( "injected queries from input", injectedConfigMacro, { @@ -649,7 +675,7 @@ test( }, ); -test( +test.serial( "injected queries from input overrides", injectedConfigMacro, { @@ -673,7 +699,7 @@ test( }, ); -test( +test.serial( "injected queries from input combines", injectedConfigMacro, { @@ -701,7 +727,7 @@ test( }, ); -test( +test.serial( "injected queries from input combines 2", injectedConfigMacro, { @@ -723,7 +749,7 @@ test( }, ); -test( +test.serial( "injected queries and packs, but empty", injectedConfigMacro, { @@ -742,7 +768,7 @@ test( {}, ); -test( +test.serial( "repo property queries have the highest precedence", injectedConfigMacro, { @@ -764,7 +790,7 @@ test( }, ); -test( +test.serial( "repo property queries combines with queries input", injectedConfigMacro, { @@ -791,7 +817,7 @@ test( }, ); -test( +test.serial( "repo property queries combines everything else", injectedConfigMacro, { @@ -820,55 +846,61 @@ test( }, ); -test("passes a code scanning config AND qlconfig to the CLI", async (t: ExecutionContext) => { - await util.withTmpDir(async (tempDir) => { - const runnerConstructorStub = stubToolRunnerConstructor(); - const codeqlObject = await stubCodeql(); - await codeqlObject.databaseInitCluster( - { ...stubConfig, tempDir }, - "", - undefined, - "/path/to/qlconfig.yml", - getRunnerLogger(true), - ); +test.serial( + "passes a code scanning config AND qlconfig to the CLI", + async (t: ExecutionContext) => { + await util.withTmpDir(async (tempDir) => { + const runnerConstructorStub = stubToolRunnerConstructor(); + const codeqlObject = await stubCodeql(); + await codeqlObject.databaseInitCluster( + { ...stubConfig, tempDir }, + "", + undefined, + "/path/to/qlconfig.yml", + getRunnerLogger(true), + ); - const args = runnerConstructorStub.firstCall.args[1] as string[]; - // should have used a config file - const hasCodeScanningConfigArg = args.some((arg: string) => - arg.startsWith("--codescanning-config="), - ); - t.true(hasCodeScanningConfigArg, "Should have injected a qlconfig"); + const args = runnerConstructorStub.firstCall.args[1] as string[]; + // should have used a config file + const hasCodeScanningConfigArg = args.some((arg: string) => + arg.startsWith("--codescanning-config="), + ); + t.true(hasCodeScanningConfigArg, "Should have injected a qlconfig"); - // should have passed a qlconfig file - const hasQlconfigArg = args.some((arg: string) => - arg.startsWith("--qlconfig-file="), - ); - t.truthy(hasQlconfigArg, "Should have injected a codescanning config"); - }); -}); + // should have passed a qlconfig file + const hasQlconfigArg = args.some((arg: string) => + arg.startsWith("--qlconfig-file="), + ); + t.truthy(hasQlconfigArg, "Should have injected a codescanning config"); + }); + }, +); -test("does not pass a qlconfig to the CLI when it is undefined", async (t: ExecutionContext) => { - await util.withTmpDir(async (tempDir) => { - const runnerConstructorStub = stubToolRunnerConstructor(); - const codeqlObject = await stubCodeql(); +test.serial( + "does not pass a qlconfig to the CLI when it is undefined", + async (t: ExecutionContext) => { + await util.withTmpDir(async (tempDir) => { + const runnerConstructorStub = stubToolRunnerConstructor(); + const codeqlObject = await stubCodeql(); - await codeqlObject.databaseInitCluster( - { ...stubConfig, tempDir }, - "", - undefined, - undefined, // undefined qlconfigFile - getRunnerLogger(true), - ); + await codeqlObject.databaseInitCluster( + { ...stubConfig, tempDir }, + "", + undefined, + undefined, // undefined qlconfigFile + getRunnerLogger(true), + ); - const args = runnerConstructorStub.firstCall.args[1] as any[]; - const hasQlconfigArg = args.some((arg: string) => - arg.startsWith("--qlconfig-file="), - ); - t.false(hasQlconfigArg, "should NOT have injected a qlconfig"); - }); -}); + const args = runnerConstructorStub.firstCall.args[1] as any[]; + const hasQlconfigArg = args.some((arg: string) => + arg.startsWith("--qlconfig-file="), + ); + t.false(hasQlconfigArg, "should NOT have injected a qlconfig"); + }); + }, +); -test("runTool summarizes several fatal errors", async (t) => { +test.serial("runTool summarizes several fatal errors", async (t) => { const heapError = "A fatal error occurred: Evaluator heap must be at least 384.00 MiB"; const datasetImportError = @@ -905,7 +937,7 @@ test("runTool summarizes several fatal errors", async (t) => { ); }); -test("runTool summarizes autobuilder errors", async (t) => { +test.serial("runTool summarizes autobuilder errors", async (t) => { const stderr = ` [2019-09-18 12:00:00] [autobuild] A non-error message [2019-09-18 12:00:00] Untagged message @@ -938,7 +970,7 @@ test("runTool summarizes autobuilder errors", async (t) => { ); }); -test("runTool truncates long autobuilder errors", async (t) => { +test.serial("runTool truncates long autobuilder errors", async (t) => { const stderr = Array.from( { length: 20 }, (_, i) => `[2019-09-18 12:00:00] [autobuild] [ERROR] line${i + 1}`, @@ -964,7 +996,7 @@ test("runTool truncates long autobuilder errors", async (t) => { ); }); -test("runTool recognizes fatal internal errors", async (t) => { +test.serial("runTool recognizes fatal internal errors", async (t) => { const stderr = ` [11/31 eval 8m19s] Evaluation done; writing results to codeql/go-queries/Security/CWE-020/MissingRegexpAnchor.bqrs. Oops! A fatal internal error occurred. Details: @@ -989,64 +1021,70 @@ test("runTool recognizes fatal internal errors", async (t) => { ); }); -test("runTool outputs last line of stderr if fatal error could not be found", async (t) => { - const cliStderr = "line1\nline2\nline3\nline4\nline5"; - stubToolRunnerConstructor(32, cliStderr); - const codeqlObject = await stubCodeql(); - // io throws because of the test CodeQL object. - sinon.stub(io, "which").resolves(""); - - await t.throwsAsync( - async () => - await codeqlObject.finalizeDatabase( - "db", - "--threads=2", - "--ram=2048", - false, - ), - { - instanceOf: util.ConfigurationError, - message: new RegExp( - 'Encountered a fatal error while running \\"codeql-for-testing database finalize --finalize-dataset --threads=2 --ram=2048 db\\"\\. ' + - "Exit code was 32 and last log line was: line5\\. See the logs for more details\\.", - ), - }, - ); -}); +test.serial( + "runTool outputs last line of stderr if fatal error could not be found", + async (t) => { + const cliStderr = "line1\nline2\nline3\nline4\nline5"; + stubToolRunnerConstructor(32, cliStderr); + const codeqlObject = await stubCodeql(); + // io throws because of the test CodeQL object. + sinon.stub(io, "which").resolves(""); + + await t.throwsAsync( + async () => + await codeqlObject.finalizeDatabase( + "db", + "--threads=2", + "--ram=2048", + false, + ), + { + instanceOf: util.ConfigurationError, + message: new RegExp( + 'Encountered a fatal error while running \\"codeql-for-testing database finalize --finalize-dataset --threads=2 --ram=2048 db\\"\\. ' + + "Exit code was 32 and last log line was: line5\\. See the logs for more details\\.", + ), + }, + ); + }, +); -test("Avoids duplicating --overwrite flag if specified in CODEQL_ACTION_EXTRA_OPTIONS", async (t) => { - const runnerConstructorStub = stubToolRunnerConstructor(); - const codeqlObject = await stubCodeql(); - // io throws because of the test CodeQL object. - sinon.stub(io, "which").resolves(""); +test.serial( + "Avoids duplicating --overwrite flag if specified in CODEQL_ACTION_EXTRA_OPTIONS", + async (t) => { + const runnerConstructorStub = stubToolRunnerConstructor(); + const codeqlObject = await stubCodeql(); + // io throws because of the test CodeQL object. + sinon.stub(io, "which").resolves(""); - process.env["CODEQL_ACTION_EXTRA_OPTIONS"] = - '{ "database": { "init": ["--overwrite"] } }'; + process.env["CODEQL_ACTION_EXTRA_OPTIONS"] = + '{ "database": { "init": ["--overwrite"] } }'; - await codeqlObject.databaseInitCluster( - stubConfig, - "sourceRoot", - undefined, - undefined, - getRunnerLogger(false), - ); + await codeqlObject.databaseInitCluster( + stubConfig, + "sourceRoot", + undefined, + undefined, + getRunnerLogger(false), + ); - t.true(runnerConstructorStub.calledOnce); - const args = runnerConstructorStub.firstCall.args[1] as string[]; - t.is( - args.filter((option: string) => option === "--overwrite").length, - 1, - "--overwrite should only be passed once", - ); + t.true(runnerConstructorStub.calledOnce); + const args = runnerConstructorStub.firstCall.args[1] as string[]; + t.is( + args.filter((option: string) => option === "--overwrite").length, + 1, + "--overwrite should only be passed once", + ); - // Clean up - const configArg = args.find((arg: string) => - arg.startsWith("--codescanning-config="), - ); - t.truthy(configArg, "Should have injected a codescanning config"); - const configFile = configArg!.split("=")[1]; - await fs.promises.rm(configFile, { force: true }); -}); + // Clean up + const configArg = args.find((arg: string) => + arg.startsWith("--codescanning-config="), + ); + t.truthy(configArg, "Should have injected a codescanning config"); + const configFile = configArg!.split("=")[1]; + await fs.promises.rm(configFile, { force: true }); + }, +); export function stubToolRunnerConstructor( exitCode: number = 0, diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 6f780b29bf..4ab7a17485 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -137,7 +137,7 @@ function mockListLanguages(languages: string[]) { sinon.stub(api, "getApiClient").value(() => client); } -test("load empty config", async (t) => { +test.serial("load empty config", async (t) => { return await withTmpDir(async (tempDir) => { const logger = getRunnerLogger(true); const languages = "javascript,python"; @@ -178,7 +178,7 @@ test("load empty config", async (t) => { }); }); -test("load code quality config", async (t) => { +test.serial("load code quality config", async (t) => { return await withTmpDir(async (tempDir) => { const logger = getRunnerLogger(true); const languages = "actions"; @@ -228,65 +228,68 @@ test("load code quality config", async (t) => { }); }); -test("initActionState doesn't throw if there are queries configured in the repository properties", async (t) => { - return await withTmpDir(async (tempDir) => { - const logger = getRunnerLogger(true); - const languages = "javascript"; - - const codeql = createStubCodeQL({ - async betterResolveLanguages() { - return { - extractors: { - javascript: [{ extractor_root: "" }], - }, - }; - }, - }); +test.serial( + "initActionState doesn't throw if there are queries configured in the repository properties", + async (t) => { + return await withTmpDir(async (tempDir) => { + const logger = getRunnerLogger(true); + const languages = "javascript"; - // This should be ignored and no error should be thrown. - const repositoryProperties = { - "github-codeql-extra-queries": "+foo", - }; + const codeql = createStubCodeQL({ + async betterResolveLanguages() { + return { + extractors: { + javascript: [{ extractor_root: "" }], + }, + }; + }, + }); - // Expected configuration for a CQ-only analysis. - const computedConfig: UserConfig = { - "disable-default-queries": true, - queries: [{ uses: "code-quality" }], - "query-filters": [], - }; + // This should be ignored and no error should be thrown. + const repositoryProperties = { + "github-codeql-extra-queries": "+foo", + }; - const expectedConfig = createTestConfig({ - analysisKinds: [AnalysisKind.CodeQuality], - languages: [KnownLanguage.javascript], - codeQLCmd: codeql.getPath(), - computedConfig, - dbLocation: path.resolve(tempDir, "codeql_databases"), - debugArtifactName: "", - debugDatabaseName: "", - tempDir, - repositoryProperties, - }); + // Expected configuration for a CQ-only analysis. + const computedConfig: UserConfig = { + "disable-default-queries": true, + queries: [{ uses: "code-quality" }], + "query-filters": [], + }; - await t.notThrowsAsync(async () => { - const config = await configUtils.initConfig( - createFeatures([]), - createTestInitConfigInputs({ - analysisKinds: [AnalysisKind.CodeQuality], - languagesInput: languages, - repository: { owner: "github", repo: "example" }, - tempDir, - codeql, - repositoryProperties, - logger, - }), - ); + const expectedConfig = createTestConfig({ + analysisKinds: [AnalysisKind.CodeQuality], + languages: [KnownLanguage.javascript], + codeQLCmd: codeql.getPath(), + computedConfig, + dbLocation: path.resolve(tempDir, "codeql_databases"), + debugArtifactName: "", + debugDatabaseName: "", + tempDir, + repositoryProperties, + }); + + await t.notThrowsAsync(async () => { + const config = await configUtils.initConfig( + createFeatures([]), + createTestInitConfigInputs({ + analysisKinds: [AnalysisKind.CodeQuality], + languagesInput: languages, + repository: { owner: "github", repo: "example" }, + tempDir, + codeql, + repositoryProperties, + logger, + }), + ); - t.deepEqual(config, expectedConfig); + t.deepEqual(config, expectedConfig); + }); }); - }); -}); + }, +); -test("loading a saved config produces the same config", async (t) => { +test.serial("loading a saved config produces the same config", async (t) => { return await withTmpDir(async (tempDir) => { const logger = getRunnerLogger(true); @@ -333,7 +336,7 @@ test("loading a saved config produces the same config", async (t) => { }); }); -test("loading config with version mismatch throws", async (t) => { +test.serial("loading config with version mismatch throws", async (t) => { return await withTmpDir(async (tempDir) => { const logger = getRunnerLogger(true); @@ -385,7 +388,7 @@ test("loading config with version mismatch throws", async (t) => { }); }); -test("load input outside of workspace", async (t) => { +test.serial("load input outside of workspace", async (t) => { return await withTmpDir(async (tempDir) => { try { await configUtils.initConfig( @@ -410,7 +413,7 @@ test("load input outside of workspace", async (t) => { }); }); -test("load non-local input with invalid repo syntax", async (t) => { +test.serial("load non-local input with invalid repo syntax", async (t) => { return await withTmpDir(async (tempDir) => { // no filename given, just a repo const configFile = "octo-org/codeql-config@main"; @@ -438,7 +441,7 @@ test("load non-local input with invalid repo syntax", async (t) => { }); }); -test("load non-existent input", async (t) => { +test.serial("load non-existent input", async (t) => { return await withTmpDir(async (tempDir) => { const languagesInput = "javascript"; const configFile = "input"; @@ -468,7 +471,7 @@ test("load non-existent input", async (t) => { }); }); -test("load non-empty input", async (t) => { +test.serial("load non-empty input", async (t) => { return await withTmpDir(async (tempDir) => { const codeql = createStubCodeQL({ async betterResolveLanguages() { @@ -539,18 +542,20 @@ test("load non-empty input", async (t) => { }); }); -test("Using config input and file together, config input should be used.", async (t) => { - return await withTmpDir(async (tempDir) => { - process.env["RUNNER_TEMP"] = tempDir; - process.env["GITHUB_WORKSPACE"] = tempDir; +test.serial( + "Using config input and file together, config input should be used.", + async (t) => { + return await withTmpDir(async (tempDir) => { + process.env["RUNNER_TEMP"] = tempDir; + process.env["GITHUB_WORKSPACE"] = tempDir; - const inputFileContents = ` + const inputFileContents = ` name: my config queries: - uses: ./foo_file`; - const configFilePath = createConfigFile(inputFileContents, tempDir); + const configFilePath = createConfigFile(inputFileContents, tempDir); - const configInput = ` + const configInput = ` name: my config queries: - uses: ./foo @@ -561,39 +566,40 @@ test("Using config input and file together, config input should be used.", async - c/d@1.2.3 `; - fs.mkdirSync(path.join(tempDir, "foo")); + fs.mkdirSync(path.join(tempDir, "foo")); - const codeql = createStubCodeQL({ - async betterResolveLanguages() { - return { - extractors: { - javascript: [{ extractor_root: "" }], - python: [{ extractor_root: "" }], - }, - }; - }, - }); + const codeql = createStubCodeQL({ + async betterResolveLanguages() { + return { + extractors: { + javascript: [{ extractor_root: "" }], + python: [{ extractor_root: "" }], + }, + }; + }, + }); - // Only JS, python packs will be ignored - const languagesInput = "javascript"; + // Only JS, python packs will be ignored + const languagesInput = "javascript"; - const config = await configUtils.initConfig( - createFeatures([]), - createTestInitConfigInputs({ - languagesInput, - configFile: configFilePath, - configInput, - tempDir, - codeql, - workspacePath: tempDir, - }), - ); + const config = await configUtils.initConfig( + createFeatures([]), + createTestInitConfigInputs({ + languagesInput, + configFile: configFilePath, + configInput, + tempDir, + codeql, + workspacePath: tempDir, + }), + ); - t.deepEqual(config.originalUserInput, yaml.load(configInput)); - }); -}); + t.deepEqual(config.originalUserInput, yaml.load(configInput)); + }); + }, +); -test("API client used when reading remote config", async (t) => { +test.serial("API client used when reading remote config", async (t) => { return await withTmpDir(async (tempDir) => { const codeql = createStubCodeQL({ async betterResolveLanguages() { @@ -642,34 +648,37 @@ test("API client used when reading remote config", async (t) => { }); }); -test("Remote config handles the case where a directory is provided", async (t) => { - return await withTmpDir(async (tempDir) => { - const dummyResponse = []; // directories are returned as arrays - mockGetContents(dummyResponse); +test.serial( + "Remote config handles the case where a directory is provided", + async (t) => { + return await withTmpDir(async (tempDir) => { + const dummyResponse = []; // directories are returned as arrays + mockGetContents(dummyResponse); - const repoReference = "octo-org/codeql-config/config.yaml@main"; - try { - await configUtils.initConfig( - createFeatures([]), - createTestInitConfigInputs({ - configFile: repoReference, - tempDir, - workspacePath: tempDir, - }), - ); - throw new Error("initConfig did not throw error"); - } catch (err) { - t.deepEqual( - err, - new ConfigurationError( - errorMessages.getConfigFileDirectoryGivenMessage(repoReference), - ), - ); - } - }); -}); + const repoReference = "octo-org/codeql-config/config.yaml@main"; + try { + await configUtils.initConfig( + createFeatures([]), + createTestInitConfigInputs({ + configFile: repoReference, + tempDir, + workspacePath: tempDir, + }), + ); + throw new Error("initConfig did not throw error"); + } catch (err) { + t.deepEqual( + err, + new ConfigurationError( + errorMessages.getConfigFileDirectoryGivenMessage(repoReference), + ), + ); + } + }); + }, +); -test("Invalid format of remote config handled correctly", async (t) => { +test.serial("Invalid format of remote config handled correctly", async (t) => { return await withTmpDir(async (tempDir) => { const dummyResponse = { // note no "content" property here @@ -698,7 +707,7 @@ test("Invalid format of remote config handled correctly", async (t) => { }); }); -test("No detected languages", async (t) => { +test.serial("No detected languages", async (t) => { return await withTmpDir(async (tempDir) => { mockListLanguages([]); const codeql = createStubCodeQL({ @@ -726,7 +735,7 @@ test("No detected languages", async (t) => { }); }); -test("Unknown languages", async (t) => { +test.serial("Unknown languages", async (t) => { return await withTmpDir(async (tempDir) => { const languagesInput = "rubbish,english"; @@ -753,7 +762,7 @@ test("Unknown languages", async (t) => { const mockLogger = getRunnerLogger(true); -test("no generateRegistries when registries is undefined", async (t) => { +test.serial("no generateRegistries when registries is undefined", async (t) => { return await withTmpDir(async (tmpDir) => { const registriesInput = undefined; const logger = getRunnerLogger(true); @@ -765,24 +774,27 @@ test("no generateRegistries when registries is undefined", async (t) => { }); }); -test("generateRegistries prefers original CODEQL_REGISTRIES_AUTH", async (t) => { - return await withTmpDir(async (tmpDir) => { - process.env.CODEQL_REGISTRIES_AUTH = "original"; - const registriesInput = yaml.dump([ - { - url: "http://ghcr.io", - packages: ["codeql/*", "codeql-testing/*"], - token: "not-a-token", - }, - ]); - const logger = getRunnerLogger(true); - const { registriesAuthTokens, qlconfigFile } = - await configUtils.generateRegistries(registriesInput, tmpDir, logger); +test.serial( + "generateRegistries prefers original CODEQL_REGISTRIES_AUTH", + async (t) => { + return await withTmpDir(async (tmpDir) => { + process.env.CODEQL_REGISTRIES_AUTH = "original"; + const registriesInput = yaml.dump([ + { + url: "http://ghcr.io", + packages: ["codeql/*", "codeql-testing/*"], + token: "not-a-token", + }, + ]); + const logger = getRunnerLogger(true); + const { registriesAuthTokens, qlconfigFile } = + await configUtils.generateRegistries(registriesInput, tmpDir, logger); - t.is(registriesAuthTokens, "original"); - t.is(qlconfigFile, path.join(tmpDir, "qlconfig.yml")); - }); -}); + t.is(registriesAuthTokens, "original"); + t.is(qlconfigFile, path.join(tmpDir, "qlconfig.yml")); + }); + }, +); // getLanguages @@ -860,7 +872,7 @@ const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); expectedLanguages: ["javascript"], }, ].forEach((args) => { - test(`getLanguages: ${args.name}`, async (t) => { + test.serial(`getLanguages: ${args.name}`, async (t) => { const mockRequest = mockLanguagesInRepo(args.languagesInRepository); const stubExtractorEntry = { extractor_root: "", @@ -930,46 +942,55 @@ for (const { displayName, language, feature } of [ feature: Feature.DisableCsharpBuildless, }, ]) { - test(`Build mode not overridden when disable ${displayName} buildless feature flag disabled`, async (t) => { - const messages: LoggedMessage[] = []; - const buildMode = await configUtils.parseBuildModeInput( - "none", - [language], - createFeatures([]), - getRecordingLogger(messages), - ); - t.is(buildMode, BuildMode.None); - t.deepEqual(messages, []); - }); + test.serial( + `Build mode not overridden when disable ${displayName} buildless feature flag disabled`, + async (t) => { + const messages: LoggedMessage[] = []; + const buildMode = await configUtils.parseBuildModeInput( + "none", + [language], + createFeatures([]), + getRecordingLogger(messages), + ); + t.is(buildMode, BuildMode.None); + t.deepEqual(messages, []); + }, + ); - test(`Build mode not overridden for other languages when disable ${displayName} buildless feature flag enabled`, async (t) => { - const messages: LoggedMessage[] = []; - const buildMode = await configUtils.parseBuildModeInput( - "none", - [KnownLanguage.python], - createFeatures([feature]), - getRecordingLogger(messages), - ); - t.is(buildMode, BuildMode.None); - t.deepEqual(messages, []); - }); + test.serial( + `Build mode not overridden for other languages when disable ${displayName} buildless feature flag enabled`, + async (t) => { + const messages: LoggedMessage[] = []; + const buildMode = await configUtils.parseBuildModeInput( + "none", + [KnownLanguage.python], + createFeatures([feature]), + getRecordingLogger(messages), + ); + t.is(buildMode, BuildMode.None); + t.deepEqual(messages, []); + }, + ); - test(`Build mode overridden when analyzing ${displayName} and disable ${displayName} buildless feature flag enabled`, async (t) => { - const messages: LoggedMessage[] = []; - const buildMode = await configUtils.parseBuildModeInput( - "none", - [language], - createFeatures([feature]), - getRecordingLogger(messages), - ); - t.is(buildMode, BuildMode.Autobuild); - t.deepEqual(messages, [ - { - message: `Scanning ${displayName} code without a build is temporarily unavailable. Falling back to 'autobuild' build mode.`, - type: "warning", - }, - ]); - }); + test.serial( + `Build mode overridden when analyzing ${displayName} and disable ${displayName} buildless feature flag enabled`, + async (t) => { + const messages: LoggedMessage[] = []; + const buildMode = await configUtils.parseBuildModeInput( + "none", + [language], + createFeatures([feature]), + getRecordingLogger(messages), + ); + t.is(buildMode, BuildMode.Autobuild); + t.deepEqual(messages, [ + { + message: `Scanning ${displayName} code without a build is temporarily unavailable. Falling back to 'autobuild' build mode.`, + type: "warning", + }, + ]); + }, + ); } interface OverlayDatabaseModeTestSetup { @@ -1106,7 +1127,7 @@ const getOverlayDatabaseModeMacro = test.macro({ title: (_, title) => `getOverlayDatabaseMode: ${title}`, }); -test( +test.serial( getOverlayDatabaseModeMacro, "Environment variable override - Overlay", { @@ -1118,7 +1139,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Environment variable override - OverlayBase", { @@ -1130,7 +1151,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Environment variable override - None", { @@ -1142,7 +1163,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Ignore invalid environment variable", { @@ -1155,7 +1176,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Ignore feature flag when analyzing non-default branch", { @@ -1168,7 +1189,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch when feature enabled", { @@ -1182,7 +1203,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch when feature enabled with custom analysis", { @@ -1199,7 +1220,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch when code-scanning feature enabled", { @@ -1216,7 +1237,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch if runner disk space is too low", { @@ -1238,7 +1259,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch if we can't determine runner disk space", { @@ -1257,7 +1278,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch if runner disk space is too low and skip resource checks flag is enabled", { @@ -1279,7 +1300,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch if runner disk space is below v2 limit and v2 resource checks enabled", { @@ -1302,7 +1323,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks enabled", { @@ -1324,7 +1345,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks not enabled", { @@ -1346,7 +1367,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch if memory flag is too low", { @@ -1365,7 +1386,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch if memory flag is too low but CodeQL >= 2.24.3", { @@ -1384,7 +1405,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay-base database on default branch if memory flag is too low and skip resource checks flag is enabled", { @@ -1403,7 +1424,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when cached status indicates previous failure", { @@ -1423,7 +1444,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when cached status indicates previous failure", { @@ -1443,7 +1464,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when code-scanning feature enabled with disable-default-queries", { @@ -1464,7 +1485,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when code-scanning feature enabled with packs", { @@ -1485,7 +1506,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when code-scanning feature enabled with queries", { @@ -1506,7 +1527,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when code-scanning feature enabled with query-filters", { @@ -1527,7 +1548,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when only language-specific feature enabled", { @@ -1542,7 +1563,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when only code-scanning feature enabled", { @@ -1557,7 +1578,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay-base database on default branch when language-specific feature disabled", { @@ -1572,7 +1593,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay analysis on PR when feature enabled", { @@ -1586,7 +1607,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay analysis on PR when feature enabled with custom analysis", { @@ -1603,7 +1624,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay analysis on PR when code-scanning feature enabled", { @@ -1620,7 +1641,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR if runner disk space is too low", { @@ -1642,7 +1663,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay analysis on PR if runner disk space is too low and skip resource checks flag is enabled", { @@ -1664,7 +1685,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR if we can't determine runner disk space", { @@ -1683,7 +1704,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR if memory flag is too low", { @@ -1702,7 +1723,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay analysis on PR if memory flag is too low but CodeQL >= 2.24.3", { @@ -1721,7 +1742,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay analysis on PR if memory flag is too low and skip resource checks flag is enabled", { @@ -1740,7 +1761,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when code-scanning feature enabled with disable-default-queries", { @@ -1761,7 +1782,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when code-scanning feature enabled with packs", { @@ -1782,7 +1803,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when code-scanning feature enabled with queries", { @@ -1803,7 +1824,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when code-scanning feature enabled with query-filters", { @@ -1824,7 +1845,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when only language-specific feature enabled", { @@ -1839,7 +1860,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when only code-scanning feature enabled", { @@ -1854,7 +1875,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay analysis on PR when language-specific feature disabled", { @@ -1869,7 +1890,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay PR analysis by env", { @@ -1881,7 +1902,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay PR analysis by env on a runner with low disk space", { @@ -1894,7 +1915,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay PR analysis by feature flag", { @@ -1908,7 +1929,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Fallback due to autobuild with traced language", { @@ -1923,7 +1944,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Fallback due to no build mode with traced language", { @@ -1938,7 +1959,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Fallback due to old CodeQL version", { @@ -1952,7 +1973,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Fallback due to missing git root", { @@ -1966,7 +1987,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Fallback due to old git version", { @@ -1980,7 +2001,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Fallback when git version cannot be determined", { @@ -1994,7 +2015,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "No overlay when disabled via repository property", { @@ -2012,7 +2033,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Overlay not disabled when repository property is false", { @@ -2029,7 +2050,7 @@ test( }, ); -test( +test.serial( getOverlayDatabaseModeMacro, "Environment variable override takes precedence over repository property", { @@ -2046,7 +2067,7 @@ test( // Exercise language-specific overlay analysis features code paths for (const language in KnownLanguage) { - test( + test.serial( getOverlayDatabaseModeMacro, `Check default overlay analysis feature for ${language}`, { @@ -2062,13 +2083,16 @@ for (const language in KnownLanguage) { ); } -test("hasActionsWorkflows doesn't throw if workflows folder doesn't exist", async (t) => { - return withTmpDir(async (tmpDir) => { - t.notThrows(() => configUtils.hasActionsWorkflows(tmpDir)); - }); -}); +test.serial( + "hasActionsWorkflows doesn't throw if workflows folder doesn't exist", + async (t) => { + return withTmpDir(async (tmpDir) => { + t.notThrows(() => configUtils.hasActionsWorkflows(tmpDir)); + }); + }, +); -test("getPrimaryAnalysisConfig - single analysis kind", (t) => { +test.serial("getPrimaryAnalysisConfig - single analysis kind", (t) => { // If only one analysis kind is configured, we expect to get the matching configuration. for (const analysisKind of supportedAnalysisKinds) { const singleKind = createTestConfig({ analysisKinds: [analysisKind] }); @@ -2076,7 +2100,7 @@ test("getPrimaryAnalysisConfig - single analysis kind", (t) => { } }); -test("getPrimaryAnalysisConfig - Code Scanning + Code Quality", (t) => { +test.serial("getPrimaryAnalysisConfig - Code Scanning + Code Quality", (t) => { // For CS+CQ, we expect to get the Code Scanning configuration. const codeScanningAndCodeQuality = createTestConfig({ analysisKinds: [AnalysisKind.CodeScanning, AnalysisKind.CodeQuality], diff --git a/src/database-upload.test.ts b/src/database-upload.test.ts index 57d07adb28..3d8433d8b5 100644 --- a/src/database-upload.test.ts +++ b/src/database-upload.test.ts @@ -82,70 +82,76 @@ function getCodeQL() { }); } -test("Abort database upload if 'upload-database' input set to false", async (t) => { - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - sinon - .stub(actionsUtil, "getRequiredInput") - .withArgs("upload-database") - .returns("false"); - sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); - - const loggedMessages = []; - await cleanupAndUploadDatabases( - testRepoName, - getCodeQL(), - getTestConfig(tmpDir), - testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), - ); - t.assert( - loggedMessages.find( - (v: LoggedMessage) => - v.type === "debug" && - v.message === - "Database upload disabled in workflow. Skipping upload.", - ) !== undefined, - ); - }); -}); - -test("Abort database upload if 'analysis-kinds: code-scanning' is not enabled", async (t) => { - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - sinon - .stub(actionsUtil, "getRequiredInput") - .withArgs("upload-database") - .returns("true"); - sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); - - await mockHttpRequests(201); - - const loggedMessages = []; - await cleanupAndUploadDatabases( - testRepoName, - getCodeQL(), - { - ...getTestConfig(tmpDir), - analysisKinds: [AnalysisKind.CodeQuality], - }, - testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), - ); - t.assert( - loggedMessages.find( - (v: LoggedMessage) => - v.type === "debug" && - v.message === - "Not uploading database because 'analysis-kinds: code-scanning' is not enabled.", - ) !== undefined, - ); - }); -}); - -test("Abort database upload if running against GHES", async (t) => { +test.serial( + "Abort database upload if 'upload-database' input set to false", + async (t) => { + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + sinon + .stub(actionsUtil, "getRequiredInput") + .withArgs("upload-database") + .returns("false"); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); + + const loggedMessages = []; + await cleanupAndUploadDatabases( + testRepoName, + getCodeQL(), + getTestConfig(tmpDir), + testApiDetails, + createFeatures([]), + getRecordingLogger(loggedMessages), + ); + t.assert( + loggedMessages.find( + (v: LoggedMessage) => + v.type === "debug" && + v.message === + "Database upload disabled in workflow. Skipping upload.", + ) !== undefined, + ); + }); + }, +); + +test.serial( + "Abort database upload if 'analysis-kinds: code-scanning' is not enabled", + async (t) => { + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + sinon + .stub(actionsUtil, "getRequiredInput") + .withArgs("upload-database") + .returns("true"); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); + + await mockHttpRequests(201); + + const loggedMessages = []; + await cleanupAndUploadDatabases( + testRepoName, + getCodeQL(), + { + ...getTestConfig(tmpDir), + analysisKinds: [AnalysisKind.CodeQuality], + }, + testApiDetails, + createFeatures([]), + getRecordingLogger(loggedMessages), + ); + t.assert( + loggedMessages.find( + (v: LoggedMessage) => + v.type === "debug" && + v.message === + "Not uploading database because 'analysis-kinds: code-scanning' is not enabled.", + ) !== undefined, + ); + }); + }, +); + +test.serial("Abort database upload if running against GHES", async (t) => { await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); sinon @@ -177,35 +183,38 @@ test("Abort database upload if running against GHES", async (t) => { }); }); -test("Abort database upload if not analyzing default branch", async (t) => { - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - sinon - .stub(actionsUtil, "getRequiredInput") - .withArgs("upload-database") - .returns("true"); - sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); - - const loggedMessages = []; - await cleanupAndUploadDatabases( - testRepoName, - getCodeQL(), - getTestConfig(tmpDir), - testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), - ); - t.assert( - loggedMessages.find( - (v: LoggedMessage) => - v.type === "debug" && - v.message === "Not analyzing default branch. Skipping upload.", - ) !== undefined, - ); - }); -}); - -test("Don't crash if uploading a database fails", async (t) => { +test.serial( + "Abort database upload if not analyzing default branch", + async (t) => { + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + sinon + .stub(actionsUtil, "getRequiredInput") + .withArgs("upload-database") + .returns("true"); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); + + const loggedMessages = []; + await cleanupAndUploadDatabases( + testRepoName, + getCodeQL(), + getTestConfig(tmpDir), + testApiDetails, + createFeatures([]), + getRecordingLogger(loggedMessages), + ); + t.assert( + loggedMessages.find( + (v: LoggedMessage) => + v.type === "debug" && + v.message === "Not analyzing default branch. Skipping upload.", + ) !== undefined, + ); + }); + }, +); + +test.serial("Don't crash if uploading a database fails", async (t) => { await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); sinon @@ -237,7 +246,7 @@ test("Don't crash if uploading a database fails", async (t) => { }); }); -test("Successfully uploading a database to github.com", async (t) => { +test.serial("Successfully uploading a database to github.com", async (t) => { await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); sinon @@ -267,7 +276,7 @@ test("Successfully uploading a database to github.com", async (t) => { }); }); -test("Successfully uploading a database to GHEC-DR", async (t) => { +test.serial("Successfully uploading a database to GHEC-DR", async (t) => { await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); sinon diff --git a/src/dependency-caching.test.ts b/src/dependency-caching.test.ts index c37d37b43b..a2d75190d1 100644 --- a/src/dependency-caching.test.ts +++ b/src/dependency-caching.test.ts @@ -44,27 +44,33 @@ function makeAbsolutePatterns(tmpDir: string, patterns: string[]): string[] { return patterns.map((pattern) => path.join(tmpDir, pattern)); } -test("getCsharpDependencyDirs - does not include BMN dir if FF is enabled", async (t) => { - await withTmpDir(async (tmpDir) => { - process.env["RUNNER_TEMP"] = tmpDir; - const codeql = createStubCodeQL({}); - const features = createFeatures([]); - - const results = await getCsharpDependencyDirs(codeql, features); - t.false(results.includes(getCsharpTempDependencyDir())); - }); -}); - -test("getCsharpDependencyDirs - includes BMN dir if FF is enabled", async (t) => { - await withTmpDir(async (tmpDir) => { - process.env["RUNNER_TEMP"] = tmpDir; - const codeql = createStubCodeQL({}); - const features = createFeatures([Feature.CsharpCacheBuildModeNone]); - - const results = await getCsharpDependencyDirs(codeql, features); - t.assert(results.includes(getCsharpTempDependencyDir())); - }); -}); +test.serial( + "getCsharpDependencyDirs - does not include BMN dir if FF is disabled", + async (t) => { + await withTmpDir(async (tmpDir) => { + process.env["RUNNER_TEMP"] = tmpDir; + const codeql = createStubCodeQL({}); + const features = createFeatures([]); + + const results = await getCsharpDependencyDirs(codeql, features); + t.false(results.includes(getCsharpTempDependencyDir())); + }); + }, +); + +test.serial( + "getCsharpDependencyDirs - includes BMN dir if FF is enabled", + async (t) => { + await withTmpDir(async (tmpDir) => { + process.env["RUNNER_TEMP"] = tmpDir; + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.CsharpCacheBuildModeNone]); + + const results = await getCsharpDependencyDirs(codeql, features); + t.assert(results.includes(getCsharpTempDependencyDir())); + }); + }, +); test("makePatternCheck - returns undefined if no patterns match", async (t) => { await withTmpDir(async (tmpDir) => { @@ -85,69 +91,81 @@ test("makePatternCheck - returns all patterns if any pattern matches", async (t) }); }); -test("getCsharpHashPatterns - returns base patterns if any pattern matches", async (t) => { - const codeql = createStubCodeQL({}); - const features = createFeatures([]); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); - makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).rejects(); - - await t.notThrowsAsync(async () => { - const result = await getCsharpHashPatterns(codeql, features); - t.deepEqual(result, CSHARP_BASE_PATTERNS); - }); -}); - -test("getCsharpHashPatterns - returns base patterns if any base pattern matches and CsharpNewCacheKey is enabled", async (t) => { - const codeql = createStubCodeQL({}); - const features = createFeatures([Feature.CsharpNewCacheKey]); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); - makePatternCheckStub - .withArgs(CSHARP_EXTRA_PATTERNS) - .resolves(CSHARP_EXTRA_PATTERNS); - - await t.notThrowsAsync(async () => { - const result = await getCsharpHashPatterns(codeql, features); - t.deepEqual(result, CSHARP_BASE_PATTERNS); - }); -}); - -test("getCsharpHashPatterns - returns extra patterns if any extra pattern matches and CsharpNewCacheKey is enabled", async (t) => { - const codeql = createStubCodeQL({}); - const features = createFeatures([Feature.CsharpNewCacheKey]); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - - makePatternCheckStub.withArgs(CSHARP_BASE_PATTERNS).resolves(undefined); - makePatternCheckStub - .withArgs(CSHARP_EXTRA_PATTERNS) - .resolves(CSHARP_EXTRA_PATTERNS); - - await t.notThrowsAsync(async () => { - const result = await getCsharpHashPatterns(codeql, features); - t.deepEqual(result, CSHARP_EXTRA_PATTERNS); - }); -}); - -test("getCsharpHashPatterns - returns undefined if neither base nor extra patterns match", async (t) => { - const codeql = createStubCodeQL({}); - const features = createFeatures([Feature.CsharpNewCacheKey]); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); +test.serial( + "getCsharpHashPatterns - returns base patterns if any pattern matches", + async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([]); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); + makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).rejects(); + + await t.notThrowsAsync(async () => { + const result = await getCsharpHashPatterns(codeql, features); + t.deepEqual(result, CSHARP_BASE_PATTERNS); + }); + }, +); + +test.serial( + "getCsharpHashPatterns - returns base patterns if any base pattern matches and CsharpNewCacheKey is enabled", + async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.CsharpNewCacheKey]); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); + makePatternCheckStub + .withArgs(CSHARP_EXTRA_PATTERNS) + .resolves(CSHARP_EXTRA_PATTERNS); + + await t.notThrowsAsync(async () => { + const result = await getCsharpHashPatterns(codeql, features); + t.deepEqual(result, CSHARP_BASE_PATTERNS); + }); + }, +); + +test.serial( + "getCsharpHashPatterns - returns extra patterns if any extra pattern matches and CsharpNewCacheKey is enabled", + async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.CsharpNewCacheKey]); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + + makePatternCheckStub.withArgs(CSHARP_BASE_PATTERNS).resolves(undefined); + makePatternCheckStub + .withArgs(CSHARP_EXTRA_PATTERNS) + .resolves(CSHARP_EXTRA_PATTERNS); + + await t.notThrowsAsync(async () => { + const result = await getCsharpHashPatterns(codeql, features); + t.deepEqual(result, CSHARP_EXTRA_PATTERNS); + }); + }, +); + +test.serial( + "getCsharpHashPatterns - returns undefined if neither base nor extra patterns match", + async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.CsharpNewCacheKey]); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub.withArgs(CSHARP_BASE_PATTERNS).resolves(undefined); - makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); + makePatternCheckStub.withArgs(CSHARP_BASE_PATTERNS).resolves(undefined); + makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); - await t.notThrowsAsync(async () => { - const result = await getCsharpHashPatterns(codeql, features); - t.deepEqual(result, undefined); - }); -}); + await t.notThrowsAsync(async () => { + const result = await getCsharpHashPatterns(codeql, features); + t.deepEqual(result, undefined); + }); + }, +); test("checkHashPatterns - logs when no patterns match", async (t) => { const codeql = createStubCodeQL({}); @@ -238,160 +256,169 @@ function makeMockCacheCheck(mockCacheKeys: string[]): RestoreCacheFunc { }; } -test("downloadDependencyCaches - does not restore caches with feature keys if no features are enabled", async (t) => { - process.env["RUNNER_OS"] = "Linux"; +test.serial( + "downloadDependencyCaches - does not restore caches with feature keys if no features are enabled", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); - sinon.stub(glob, "hashFiles").resolves("abcdef"); + sinon.stub(glob, "hashFiles").resolves("abcdef"); - const keyWithFeature = await cacheKey( - codeql, - createFeatures([Feature.CsharpNewCacheKey]), - KnownLanguage.csharp, - // Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above. - [], - ); + const keyWithFeature = await cacheKey( + codeql, + createFeatures([Feature.CsharpNewCacheKey]), + KnownLanguage.csharp, + // Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above. + [], + ); - const restoreCacheStub = sinon - .stub(actionsCache, "restoreCache") - .callsFake(makeMockCacheCheck([keyWithFeature])); + const restoreCacheStub = sinon + .stub(actionsCache, "restoreCache") + .callsFake(makeMockCacheCheck([keyWithFeature])); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); - makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); - - const result = await downloadDependencyCaches( - codeql, - createFeatures([]), - [KnownLanguage.csharp], - logger, - ); - const statusReport = result.statusReport; - t.is(statusReport.length, 1); - t.is(statusReport[0].language, KnownLanguage.csharp); - t.is(statusReport[0].hit_kind, CacheHitKind.Miss); - t.deepEqual(result.restoredKeys, []); - t.assert(restoreCacheStub.calledOnce); -}); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); + makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); -test("downloadDependencyCaches - restores caches with feature keys if features are enabled", async (t) => { - process.env["RUNNER_OS"] = "Linux"; + const result = await downloadDependencyCaches( + codeql, + createFeatures([]), + [KnownLanguage.csharp], + logger, + ); + const statusReport = result.statusReport; + t.is(statusReport.length, 1); + t.is(statusReport[0].language, KnownLanguage.csharp); + t.is(statusReport[0].hit_kind, CacheHitKind.Miss); + t.deepEqual(result.restoredKeys, []); + t.assert(restoreCacheStub.calledOnce); + }, +); + +test.serial( + "downloadDependencyCaches - restores caches with feature keys if features are enabled", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([Feature.CsharpNewCacheKey]); + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([Feature.CsharpNewCacheKey]); - const mockHash = "abcdef"; - sinon.stub(glob, "hashFiles").resolves(mockHash); + const mockHash = "abcdef"; + sinon.stub(glob, "hashFiles").resolves(mockHash); - const keyWithFeature = await cacheKey( - codeql, - features, - KnownLanguage.csharp, - // Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above. - [], - ); + const keyWithFeature = await cacheKey( + codeql, + features, + KnownLanguage.csharp, + // Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above. + [], + ); - const restoreCacheStub = sinon - .stub(actionsCache, "restoreCache") - .callsFake(makeMockCacheCheck([keyWithFeature])); + const restoreCacheStub = sinon + .stub(actionsCache, "restoreCache") + .callsFake(makeMockCacheCheck([keyWithFeature])); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); - makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); + makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); - const result = await downloadDependencyCaches( - codeql, - features, - [KnownLanguage.csharp], - logger, - ); + const result = await downloadDependencyCaches( + codeql, + features, + [KnownLanguage.csharp], + logger, + ); - // Check that the status report for telemetry indicates that one cache was restored with an exact match. - const statusReport = result.statusReport; - t.is(statusReport.length, 1); - t.is(statusReport[0].language, KnownLanguage.csharp); - t.is(statusReport[0].hit_kind, CacheHitKind.Exact); - - // Check that the restored key has been returned. - const restoredKeys = result.restoredKeys; - t.is(restoredKeys.length, 1); - t.assert( - restoredKeys[0].endsWith(mockHash), - "Expected restored key to end with hash returned by `hashFiles`", - ); + // Check that the status report for telemetry indicates that one cache was restored with an exact match. + const statusReport = result.statusReport; + t.is(statusReport.length, 1); + t.is(statusReport[0].language, KnownLanguage.csharp); + t.is(statusReport[0].hit_kind, CacheHitKind.Exact); + + // Check that the restored key has been returned. + const restoredKeys = result.restoredKeys; + t.is(restoredKeys.length, 1); + t.assert( + restoredKeys[0].endsWith(mockHash), + "Expected restored key to end with hash returned by `hashFiles`", + ); - // `restoreCache` should have been called exactly once. - t.assert(restoreCacheStub.calledOnce); -}); + // `restoreCache` should have been called exactly once. + t.assert(restoreCacheStub.calledOnce); + }, +); -test("downloadDependencyCaches - restores caches with feature keys if features are enabled for partial matches", async (t) => { - process.env["RUNNER_OS"] = "Linux"; +test.serial( + "downloadDependencyCaches - restores caches with feature keys if features are enabled for partial matches", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([Feature.CsharpNewCacheKey]); + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([Feature.CsharpNewCacheKey]); + + // We expect two calls to `hashFiles`: the first by the call to `cacheKey` below, + // and the second by `downloadDependencyCaches`. We use the result of the first + // call as part of the cache key that identifies a mock, existing cache. The result + // of the second call is for the primary restore key, which we don't want to match + // the first key so that we can test the restore keys logic. + const restoredHash = "abcdef"; + const hashFilesStub = sinon.stub(glob, "hashFiles"); + hashFilesStub.onFirstCall().resolves(restoredHash); + hashFilesStub.onSecondCall().resolves("123456"); + + const keyWithFeature = await cacheKey( + codeql, + features, + KnownLanguage.csharp, + // Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above. + [], + ); - // We expect two calls to `hashFiles`: the first by the call to `cacheKey` below, - // and the second by `downloadDependencyCaches`. We use the result of the first - // call as part of the cache key that identifies a mock, existing cache. The result - // of the second call is for the primary restore key, which we don't want to match - // the first key so that we can test the restore keys logic. - const restoredHash = "abcdef"; - const hashFilesStub = sinon.stub(glob, "hashFiles"); - hashFilesStub.onFirstCall().resolves(restoredHash); - hashFilesStub.onSecondCall().resolves("123456"); - - const keyWithFeature = await cacheKey( - codeql, - features, - KnownLanguage.csharp, - // Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above. - [], - ); + const restoreCacheStub = sinon + .stub(actionsCache, "restoreCache") + .callsFake(makeMockCacheCheck([keyWithFeature])); - const restoreCacheStub = sinon - .stub(actionsCache, "restoreCache") - .callsFake(makeMockCacheCheck([keyWithFeature])); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); + makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); - makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined); + const result = await downloadDependencyCaches( + codeql, + features, + [KnownLanguage.csharp], + logger, + ); - const result = await downloadDependencyCaches( - codeql, - features, - [KnownLanguage.csharp], - logger, - ); + // Check that the status report for telemetry indicates that one cache was restored with a partial match. + const statusReport = result.statusReport; + t.is(statusReport.length, 1); + t.is(statusReport[0].language, KnownLanguage.csharp); + t.is(statusReport[0].hit_kind, CacheHitKind.Partial); + + // Check that the restored key has been returned. + const restoredKeys = result.restoredKeys; + t.is(restoredKeys.length, 1); + t.assert( + restoredKeys[0].endsWith(restoredHash), + "Expected restored key to end with hash returned by `hashFiles`", + ); - // Check that the status report for telemetry indicates that one cache was restored with a partial match. - const statusReport = result.statusReport; - t.is(statusReport.length, 1); - t.is(statusReport[0].language, KnownLanguage.csharp); - t.is(statusReport[0].hit_kind, CacheHitKind.Partial); - - // Check that the restored key has been returned. - const restoredKeys = result.restoredKeys; - t.is(restoredKeys.length, 1); - t.assert( - restoredKeys[0].endsWith(restoredHash), - "Expected restored key to end with hash returned by `hashFiles`", - ); - - t.assert(restoreCacheStub.calledOnce); -}); + t.assert(restoreCacheStub.calledOnce); + }, +); test("uploadDependencyCaches - skips upload for a language with no cache config", async (t) => { const codeql = createStubCodeQL({}); @@ -409,148 +436,139 @@ test("uploadDependencyCaches - skips upload for a language with no cache config" ]); }); -test("uploadDependencyCaches - skips upload if no files for the hash exist", async (t) => { - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([]); - const config = createTestConfig({ - languages: [KnownLanguage.go], - }); - - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub.resolves(undefined); - - const result = await uploadDependencyCaches(codeql, features, config, logger); - t.is(result.length, 1); - t.is(result[0].language, KnownLanguage.go); - t.is(result[0].result, CacheStoreResult.NoHash); -}); - -test("uploadDependencyCaches - skips upload if we know the cache already exists", async (t) => { - process.env["RUNNER_OS"] = "Linux"; - - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([]); - - const mockHash = "abcdef"; - sinon.stub(glob, "hashFiles").resolves(mockHash); - - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); - - const primaryCacheKey = await cacheKey( - codeql, - features, - KnownLanguage.csharp, - CSHARP_BASE_PATTERNS, - ); - - const config = createTestConfig({ - languages: [KnownLanguage.csharp], - dependencyCachingRestoredKeys: [primaryCacheKey], - }); +test.serial( + "uploadDependencyCaches - skips upload if no files for the hash exist", + async (t) => { + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([]); + const config = createTestConfig({ + languages: [KnownLanguage.go], + }); - const result = await uploadDependencyCaches(codeql, features, config, logger); - t.is(result.length, 1); - t.is(result[0].language, KnownLanguage.csharp); - t.is(result[0].result, CacheStoreResult.Duplicate); -}); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub.resolves(undefined); -test("uploadDependencyCaches - skips upload if cache size is 0", async (t) => { - process.env["RUNNER_OS"] = "Linux"; + const result = await uploadDependencyCaches( + codeql, + features, + config, + logger, + ); + t.is(result.length, 1); + t.is(result[0].language, KnownLanguage.go); + t.is(result[0].result, CacheStoreResult.NoHash); + }, +); - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([]); +test.serial( + "uploadDependencyCaches - skips upload if we know the cache already exists", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; - const mockHash = "abcdef"; - sinon.stub(glob, "hashFiles").resolves(mockHash); + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([]); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); + const mockHash = "abcdef"; + sinon.stub(glob, "hashFiles").resolves(mockHash); - sinon.stub(cachingUtils, "getTotalCacheSize").resolves(0); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); - const config = createTestConfig({ - languages: [KnownLanguage.csharp], - }); + const primaryCacheKey = await cacheKey( + codeql, + features, + KnownLanguage.csharp, + CSHARP_BASE_PATTERNS, + ); - const result = await uploadDependencyCaches(codeql, features, config, logger); - t.is(result.length, 1); - t.is(result[0].language, KnownLanguage.csharp); - t.is(result[0].result, CacheStoreResult.Empty); + const config = createTestConfig({ + languages: [KnownLanguage.csharp], + dependencyCachingRestoredKeys: [primaryCacheKey], + }); - checkExpectedLogMessages(t, messages, [ - "Skipping upload of dependency cache", - ]); -}); + const result = await uploadDependencyCaches( + codeql, + features, + config, + logger, + ); + t.is(result.length, 1); + t.is(result[0].language, KnownLanguage.csharp); + t.is(result[0].result, CacheStoreResult.Duplicate); + }, +); -test("uploadDependencyCaches - uploads caches when all requirements are met", async (t) => { - process.env["RUNNER_OS"] = "Linux"; +test.serial( + "uploadDependencyCaches - skips upload if cache size is 0", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([]); + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([]); - const mockHash = "abcdef"; - sinon.stub(glob, "hashFiles").resolves(mockHash); + const mockHash = "abcdef"; + sinon.stub(glob, "hashFiles").resolves(mockHash); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); - sinon.stub(cachingUtils, "getTotalCacheSize").resolves(1024); - sinon.stub(actionsCache, "saveCache").resolves(); + sinon.stub(cachingUtils, "getTotalCacheSize").resolves(0); - const config = createTestConfig({ - languages: [KnownLanguage.csharp], - }); + const config = createTestConfig({ + languages: [KnownLanguage.csharp], + }); - const result = await uploadDependencyCaches(codeql, features, config, logger); - t.is(result.length, 1); - t.is(result[0].language, KnownLanguage.csharp); - t.is(result[0].result, CacheStoreResult.Stored); - t.is(result[0].upload_size_bytes, 1024); + const result = await uploadDependencyCaches( + codeql, + features, + config, + logger, + ); + t.is(result.length, 1); + t.is(result[0].language, KnownLanguage.csharp); + t.is(result[0].result, CacheStoreResult.Empty); - checkExpectedLogMessages(t, messages, ["Uploading cache of size"]); -}); + checkExpectedLogMessages(t, messages, [ + "Skipping upload of dependency cache", + ]); + }, +); -test("uploadDependencyCaches - catches `ReserveCacheError` exceptions", async (t) => { - process.env["RUNNER_OS"] = "Linux"; +test.serial( + "uploadDependencyCaches - uploads caches when all requirements are met", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; - const codeql = createStubCodeQL({}); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - const features = createFeatures([]); + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([]); - const mockHash = "abcdef"; - sinon.stub(glob, "hashFiles").resolves(mockHash); + const mockHash = "abcdef"; + sinon.stub(glob, "hashFiles").resolves(mockHash); - const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); - makePatternCheckStub - .withArgs(CSHARP_BASE_PATTERNS) - .resolves(CSHARP_BASE_PATTERNS); + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); - sinon.stub(cachingUtils, "getTotalCacheSize").resolves(1024); - sinon - .stub(actionsCache, "saveCache") - .throws(new actionsCache.ReserveCacheError("Already in use")); + sinon.stub(cachingUtils, "getTotalCacheSize").resolves(1024); + sinon.stub(actionsCache, "saveCache").resolves(); - const config = createTestConfig({ - languages: [KnownLanguage.csharp], - }); + const config = createTestConfig({ + languages: [KnownLanguage.csharp], + }); - await t.notThrowsAsync(async () => { const result = await uploadDependencyCaches( codeql, features, @@ -559,13 +577,57 @@ test("uploadDependencyCaches - catches `ReserveCacheError` exceptions", async (t ); t.is(result.length, 1); t.is(result[0].language, KnownLanguage.csharp); - t.is(result[0].result, CacheStoreResult.Duplicate); + t.is(result[0].result, CacheStoreResult.Stored); + t.is(result[0].upload_size_bytes, 1024); - checkExpectedLogMessages(t, messages, ["Not uploading cache for"]); - }); -}); + checkExpectedLogMessages(t, messages, ["Uploading cache of size"]); + }, +); + +test.serial( + "uploadDependencyCaches - catches `ReserveCacheError` exceptions", + async (t) => { + process.env["RUNNER_OS"] = "Linux"; + + const codeql = createStubCodeQL({}); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + const features = createFeatures([]); -test("uploadDependencyCaches - throws other exceptions", async (t) => { + const mockHash = "abcdef"; + sinon.stub(glob, "hashFiles").resolves(mockHash); + + const makePatternCheckStub = sinon.stub(internal, "makePatternCheck"); + makePatternCheckStub + .withArgs(CSHARP_BASE_PATTERNS) + .resolves(CSHARP_BASE_PATTERNS); + + sinon.stub(cachingUtils, "getTotalCacheSize").resolves(1024); + sinon + .stub(actionsCache, "saveCache") + .throws(new actionsCache.ReserveCacheError("Already in use")); + + const config = createTestConfig({ + languages: [KnownLanguage.csharp], + }); + + await t.notThrowsAsync(async () => { + const result = await uploadDependencyCaches( + codeql, + features, + config, + logger, + ); + t.is(result.length, 1); + t.is(result[0].language, KnownLanguage.csharp); + t.is(result[0].result, CacheStoreResult.Duplicate); + + checkExpectedLogMessages(t, messages, ["Not uploading cache for"]); + }); + }, +); + +test.serial("uploadDependencyCaches - throws other exceptions", async (t) => { process.env["RUNNER_OS"] = "Linux"; const codeql = createStubCodeQL({}); diff --git a/src/diff-informed-analysis-utils.test.ts b/src/diff-informed-analysis-utils.test.ts index 2d98a5f639..44b7e77917 100644 --- a/src/diff-informed-analysis-utils.test.ts +++ b/src/diff-informed-analysis-utils.test.ts @@ -97,14 +97,14 @@ const testShouldPerformDiffInformedAnalysis = test.macro({ title: (_, title) => `shouldPerformDiffInformedAnalysis: ${title}`, }); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns true in the default test case", {}, true, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns false when feature flag is disabled from the API", { @@ -113,7 +113,7 @@ test( false, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns false when CODEQL_ACTION_DIFF_INFORMED_QUERIES is set to false", { @@ -123,7 +123,7 @@ test( false, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns true when CODEQL_ACTION_DIFF_INFORMED_QUERIES is set to true", { @@ -133,7 +133,7 @@ test( true, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns false for CodeQL version 2.20.0", { @@ -142,7 +142,7 @@ test( false, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns false for invalid GHES version", { @@ -154,7 +154,7 @@ test( false, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns false for GHES version 3.18.5", { @@ -166,7 +166,7 @@ test( false, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns true for GHES version 3.19.0", { @@ -178,7 +178,7 @@ test( true, ); -test( +test.serial( testShouldPerformDiffInformedAnalysis, "returns false when not a pull request", { @@ -202,12 +202,12 @@ function runGetDiffRanges(changes: number, patch: string[] | undefined): any { ); } -test("getDiffRanges: file unchanged", async (t) => { +test.serial("getDiffRanges: file unchanged", async (t) => { const diffRanges = runGetDiffRanges(0, undefined); t.deepEqual(diffRanges, []); }); -test("getDiffRanges: file diff too large", async (t) => { +test.serial("getDiffRanges: file diff too large", async (t) => { const diffRanges = runGetDiffRanges(1000000, undefined); t.deepEqual(diffRanges, [ { @@ -218,43 +218,49 @@ test("getDiffRanges: file diff too large", async (t) => { ]); }); -test("getDiffRanges: diff thunk with single addition range", async (t) => { - const diffRanges = runGetDiffRanges(2, [ - "@@ -30,6 +50,8 @@", - " a", - " b", - " c", - "+1", - "+2", - " d", - " e", - " f", - ]); - t.deepEqual(diffRanges, [ - { - path: "/checkout/path/test.txt", - startLine: 53, - endLine: 54, - }, - ]); -}); +test.serial( + "getDiffRanges: diff thunk with single addition range", + async (t) => { + const diffRanges = runGetDiffRanges(2, [ + "@@ -30,6 +50,8 @@", + " a", + " b", + " c", + "+1", + "+2", + " d", + " e", + " f", + ]); + t.deepEqual(diffRanges, [ + { + path: "/checkout/path/test.txt", + startLine: 53, + endLine: 54, + }, + ]); + }, +); -test("getDiffRanges: diff thunk with single deletion range", async (t) => { - const diffRanges = runGetDiffRanges(2, [ - "@@ -30,8 +50,6 @@", - " a", - " b", - " c", - "-1", - "-2", - " d", - " e", - " f", - ]); - t.deepEqual(diffRanges, []); -}); +test.serial( + "getDiffRanges: diff thunk with single deletion range", + async (t) => { + const diffRanges = runGetDiffRanges(2, [ + "@@ -30,8 +50,6 @@", + " a", + " b", + " c", + "-1", + "-2", + " d", + " e", + " f", + ]); + t.deepEqual(diffRanges, []); + }, +); -test("getDiffRanges: diff thunk with single update range", async (t) => { +test.serial("getDiffRanges: diff thunk with single update range", async (t) => { const diffRanges = runGetDiffRanges(2, [ "@@ -30,7 +50,7 @@", " a", @@ -275,7 +281,7 @@ test("getDiffRanges: diff thunk with single update range", async (t) => { ]); }); -test("getDiffRanges: diff thunk with addition ranges", async (t) => { +test.serial("getDiffRanges: diff thunk with addition ranges", async (t) => { const diffRanges = runGetDiffRanges(2, [ "@@ -30,7 +50,9 @@", " a", @@ -302,7 +308,7 @@ test("getDiffRanges: diff thunk with addition ranges", async (t) => { ]); }); -test("getDiffRanges: diff thunk with mixed ranges", async (t) => { +test.serial("getDiffRanges: diff thunk with mixed ranges", async (t) => { const diffRanges = runGetDiffRanges(2, [ "@@ -30,7 +50,7 @@", " a", @@ -334,7 +340,7 @@ test("getDiffRanges: diff thunk with mixed ranges", async (t) => { ]); }); -test("getDiffRanges: multiple diff thunks", async (t) => { +test.serial("getDiffRanges: multiple diff thunks", async (t) => { const diffRanges = runGetDiffRanges(2, [ "@@ -30,6 +50,8 @@", " a", @@ -369,7 +375,7 @@ test("getDiffRanges: multiple diff thunks", async (t) => { ]); }); -test("getDiffRanges: no diff context lines", async (t) => { +test.serial("getDiffRanges: no diff context lines", async (t) => { const diffRanges = runGetDiffRanges(2, ["@@ -30 +50,2 @@", "+1", "+2"]); t.deepEqual(diffRanges, [ { @@ -380,7 +386,7 @@ test("getDiffRanges: no diff context lines", async (t) => { ]); }); -test("getDiffRanges: malformed thunk header", async (t) => { +test.serial("getDiffRanges: malformed thunk header", async (t) => { const diffRanges = runGetDiffRanges(2, ["@@ 30 +50,2 @@", "+1", "+2"]); t.deepEqual(diffRanges, undefined); }); diff --git a/src/feature-flags.test.ts b/src/feature-flags.test.ts index 8b7a0c7d59..85007df139 100644 --- a/src/feature-flags.test.ts +++ b/src/feature-flags.test.ts @@ -34,23 +34,26 @@ test.beforeEach(() => { initializeEnvironment("1.2.3"); }); -test(`All features use default values if running against GHES`, async (t) => { - await withTmpDir(async (tmpDir) => { - const loggedMessages = []; - const features = setUpFeatureFlagTests( - tmpDir, - getRecordingLogger(loggedMessages), - { type: GitHubVariant.GHES, version: "3.0.0" }, - ); +test.serial( + `All features use default values if running against GHES`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const loggedMessages = []; + const features = setUpFeatureFlagTests( + tmpDir, + getRecordingLogger(loggedMessages), + { type: GitHubVariant.GHES, version: "3.0.0" }, + ); - await assertAllFeaturesHaveDefaultValues(t, features); - checkExpectedLogMessages(t, loggedMessages, [ - "Not running against github.com. Using default values for all features.", - ]); - }); -}); + await assertAllFeaturesHaveDefaultValues(t, features); + checkExpectedLogMessages(t, loggedMessages, [ + "Not running against github.com. Using default values for all features.", + ]); + }); + }, +); -test(`Feature flags are requested in GHEC-DR`, async (t) => { +test.serial(`Feature flags are requested in GHEC-DR`, async (t) => { await withTmpDir(async (tmpDir) => { const loggedMessages = []; const features = setUpFeatureFlagTests( @@ -78,254 +81,288 @@ test(`Feature flags are requested in GHEC-DR`, async (t) => { }); }); -test("API response missing and features use default value", async (t) => { - await withTmpDir(async (tmpDir) => { - const loggedMessages: LoggedMessage[] = []; - const features = setUpFeatureFlagTests( - tmpDir, - getRecordingLogger(loggedMessages), - ); - - mockFeatureFlagApiEndpoint(403, {}); - - for (const feature of Object.values(Feature)) { - t.assert( - (await getFeatureIncludingCodeQlIfRequired(features, feature)) === - featureConfig[feature].defaultValue, - ); - } - assertAllFeaturesUndefinedInApi(t, loggedMessages); - }); -}); - -test("Features use default value if they're not returned in API response", async (t) => { - await withTmpDir(async (tmpDir) => { - const loggedMessages: LoggedMessage[] = []; - const features = setUpFeatureFlagTests( - tmpDir, - getRecordingLogger(loggedMessages), - ); - - mockFeatureFlagApiEndpoint(200, {}); - - for (const feature of Object.values(Feature)) { - t.assert( - (await getFeatureIncludingCodeQlIfRequired(features, feature)) === - featureConfig[feature].defaultValue, +test.serial( + "API response missing and features use default value", + async (t) => { + await withTmpDir(async (tmpDir) => { + const loggedMessages: LoggedMessage[] = []; + const features = setUpFeatureFlagTests( + tmpDir, + getRecordingLogger(loggedMessages), ); - } - assertAllFeaturesUndefinedInApi(t, loggedMessages); - }); -}); + mockFeatureFlagApiEndpoint(403, {}); -test("Include no more than 25 features in each API request", async (t) => { - await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); - - stubFeatureFlagApiEndpoint((request) => { - const requestedFeatures = (request.features as string).split(","); - return { - status: requestedFeatures.length <= 25 ? 200 : 400, - messageIfError: "Can request a maximum of 25 features.", - data: {}, - }; + for (const feature of Object.values(Feature)) { + t.assert( + (await getFeatureIncludingCodeQlIfRequired(features, feature)) === + featureConfig[feature].defaultValue, + ); + } + assertAllFeaturesUndefinedInApi(t, loggedMessages); }); + }, +); - // We only need to call getValue once, and it does not matter which feature - // we ask for. Under the hood, the features library will request all features - // from the API. - const feature = Object.values(Feature)[0]; - await t.notThrowsAsync(async () => - getFeatureIncludingCodeQlIfRequired(features, feature), - ); - }); -}); - -test("Feature flags exception is propagated if the API request errors", async (t) => { - await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); - - mockFeatureFlagApiEndpoint(500, {}); - - const someFeature = Object.values(Feature)[0]; - - await t.throwsAsync( - async () => getFeatureIncludingCodeQlIfRequired(features, someFeature), - { - message: - "Encountered an error while trying to determine feature enablement: Error: some error message", - }, - ); - }); -}); - -for (const feature of Object.keys(featureConfig)) { - test(`Only feature '${feature}' is enabled if enabled in the API response. Other features disabled`, async (t) => { +test.serial( + "Features use default value if they're not returned in API response", + async (t) => { await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); + const loggedMessages: LoggedMessage[] = []; + const features = setUpFeatureFlagTests( + tmpDir, + getRecordingLogger(loggedMessages), + ); - // set all features to false except the one we're testing - const expectedFeatureEnablement: { [feature: string]: boolean } = {}; - for (const f of Object.keys(featureConfig)) { - expectedFeatureEnablement[f] = f === feature; - } - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + mockFeatureFlagApiEndpoint(200, {}); - // retrieve the values of the actual features - const actualFeatureEnablement: { [feature: string]: boolean } = {}; - for (const f of Object.keys(featureConfig)) { - actualFeatureEnablement[f] = await getFeatureIncludingCodeQlIfRequired( - features, - f as Feature, + for (const feature of Object.values(Feature)) { + t.assert( + (await getFeatureIncludingCodeQlIfRequired(features, feature)) === + featureConfig[feature].defaultValue, ); } - // All features should be false except the one we're testing - t.deepEqual(actualFeatureEnablement, expectedFeatureEnablement); + assertAllFeaturesUndefinedInApi(t, loggedMessages); }); - }); + }, +); - test(`Only feature '${feature}' is enabled if the associated environment variable is true. Others disabled.`, async (t) => { +test.serial( + "Include no more than 25 features in each API request", + async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(false); - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - - // feature should be disabled initially - t.assert( - !(await getFeatureIncludingCodeQlIfRequired( - features, - feature as Feature, - )), - ); + stubFeatureFlagApiEndpoint((request) => { + const requestedFeatures = (request.features as string).split(","); + return { + status: requestedFeatures.length <= 25 ? 200 : 400, + messageIfError: "Can request a maximum of 25 features.", + data: {}, + }; + }); - // set env var to true and check that the feature is now enabled - process.env[featureConfig[feature].envVar] = "true"; - t.assert( - await getFeatureIncludingCodeQlIfRequired(features, feature as Feature), + // We only need to call getValue once, and it does not matter which feature + // we ask for. Under the hood, the features library will request all features + // from the API. + const feature = Object.values(Feature)[0]; + await t.notThrowsAsync(async () => + getFeatureIncludingCodeQlIfRequired(features, feature), ); }); - }); + }, +); - test(`Feature '${feature}' is disabled if the associated environment variable is false, even if enabled in API`, async (t) => { +test.serial( + "Feature flags exception is propagated if the API request errors", + async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(true); - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + mockFeatureFlagApiEndpoint(500, {}); - // feature should be enabled initially - t.assert( - await getFeatureIncludingCodeQlIfRequired(features, feature as Feature), - ); + const someFeature = Object.values(Feature)[0]; - // set env var to false and check that the feature is now disabled - process.env[featureConfig[feature].envVar] = "false"; - t.assert( - !(await getFeatureIncludingCodeQlIfRequired( - features, - feature as Feature, - )), + await t.throwsAsync( + async () => getFeatureIncludingCodeQlIfRequired(features, someFeature), + { + message: + "Encountered an error while trying to determine feature enablement: Error: some error message", + }, ); }); - }); + }, +); - if ( - featureConfig[feature].minimumVersion !== undefined || - featureConfig[feature].toolsFeature !== undefined - ) { - test(`Getting feature '${feature} should throw if no codeql is provided`, async (t) => { +for (const feature of Object.keys(featureConfig)) { + test.serial( + `Only feature '${feature}' is enabled if enabled in the API response. Other features disabled`, + async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(true); + // set all features to false except the one we're testing + const expectedFeatureEnablement: { [feature: string]: boolean } = {}; + for (const f of Object.keys(featureConfig)) { + expectedFeatureEnablement[f] = f === feature; + } mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - // The type system should prevent this happening, but test that if we - // bypass it we get the expected error. - await t.throwsAsync( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - async () => features.getValue(feature as any), - { - message: `Internal error: A ${ - featureConfig[feature].minimumVersion !== undefined - ? "minimum version" - : "required tools feature" - } is specified for feature ${feature}, but no instance of CodeQL was provided.`, - }, - ); + // retrieve the values of the actual features + const actualFeatureEnablement: { [feature: string]: boolean } = {}; + for (const f of Object.keys(featureConfig)) { + actualFeatureEnablement[f] = + await getFeatureIncludingCodeQlIfRequired(features, f as Feature); + } + + // All features should be false except the one we're testing + t.deepEqual(actualFeatureEnablement, expectedFeatureEnablement); }); - }); - } + }, + ); - if (featureConfig[feature].minimumVersion !== undefined) { - test(`Feature '${feature}' is disabled if the minimum CLI version is below ${featureConfig[feature].minimumVersion}`, async (t) => { + test.serial( + `Only feature '${feature}' is enabled if the associated environment variable is true. Others disabled.`, + async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(true); + const expectedFeatureEnablement = initializeFeatures(false); mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - // feature should be disabled when an old CLI version is set - let codeql = mockCodeQLVersion("2.0.0"); - t.assert(!(await features.getValue(feature as Feature, codeql))); + // feature should be disabled initially + t.assert( + !(await getFeatureIncludingCodeQlIfRequired( + features, + feature as Feature, + )), + ); - // even setting the env var to true should not enable the feature if - // the minimum CLI version is not met + // set env var to true and check that the feature is now enabled process.env[featureConfig[feature].envVar] = "true"; - t.assert(!(await features.getValue(feature as Feature, codeql))); - - // feature should be enabled when a new CLI version is set - // and env var is not set - process.env[featureConfig[feature].envVar] = ""; - codeql = mockCodeQLVersion( - featureConfig[feature].minimumVersion as string, + t.assert( + await getFeatureIncludingCodeQlIfRequired( + features, + feature as Feature, + ), ); - t.assert(await features.getValue(feature as Feature, codeql)); - - // set env var to false and check that the feature is now disabled - process.env[featureConfig[feature].envVar] = "false"; - t.assert(!(await features.getValue(feature as Feature, codeql))); }); - }); - } + }, + ); - if (featureConfig[feature].toolsFeature !== undefined) { - test(`Feature '${feature}' is disabled if the required tools feature is not enabled`, async (t) => { + test.serial( + `Feature '${feature}' is disabled if the associated environment variable is false, even if enabled in API`, + async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); const expectedFeatureEnablement = initializeFeatures(true); mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - // feature should be disabled when the required tools feature is not enabled - let codeql = mockCodeQLVersion("2.0.0"); - t.assert(!(await features.getValue(feature as Feature, codeql))); - - // even setting the env var to true should not enable the feature if - // the required tools feature is not enabled - process.env[featureConfig[feature].envVar] = "true"; - t.assert(!(await features.getValue(feature as Feature, codeql))); - - // feature should be enabled when the required tools feature is enabled - // and env var is not set - process.env[featureConfig[feature].envVar] = ""; - codeql = mockCodeQLVersion("2.0.0", { - [featureConfig[feature].toolsFeature]: true, - }); - t.assert(await features.getValue(feature as Feature, codeql)); + // feature should be enabled initially + t.assert( + await getFeatureIncludingCodeQlIfRequired( + features, + feature as Feature, + ), + ); // set env var to false and check that the feature is now disabled process.env[featureConfig[feature].envVar] = "false"; - t.assert(!(await features.getValue(feature as Feature, codeql))); + t.assert( + !(await getFeatureIncludingCodeQlIfRequired( + features, + feature as Feature, + )), + ); }); - }); + }, + ); + + if ( + featureConfig[feature].minimumVersion !== undefined || + featureConfig[feature].toolsFeature !== undefined + ) { + test.serial( + `Getting feature '${feature} should throw if no codeql is provided`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + + const expectedFeatureEnablement = initializeFeatures(true); + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + + // The type system should prevent this happening, but test that if we + // bypass it we get the expected error. + await t.throwsAsync( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + async () => features.getValue(feature as any), + { + message: `Internal error: A ${ + featureConfig[feature].minimumVersion !== undefined + ? "minimum version" + : "required tools feature" + } is specified for feature ${feature}, but no instance of CodeQL was provided.`, + }, + ); + }); + }, + ); + } + + if (featureConfig[feature].minimumVersion !== undefined) { + test.serial( + `Feature '${feature}' is disabled if the minimum CLI version is below ${featureConfig[feature].minimumVersion}`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + + const expectedFeatureEnablement = initializeFeatures(true); + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + + // feature should be disabled when an old CLI version is set + let codeql = mockCodeQLVersion("2.0.0"); + t.assert(!(await features.getValue(feature as Feature, codeql))); + + // even setting the env var to true should not enable the feature if + // the minimum CLI version is not met + process.env[featureConfig[feature].envVar] = "true"; + t.assert(!(await features.getValue(feature as Feature, codeql))); + + // feature should be enabled when a new CLI version is set + // and env var is not set + process.env[featureConfig[feature].envVar] = ""; + codeql = mockCodeQLVersion( + featureConfig[feature].minimumVersion as string, + ); + t.assert(await features.getValue(feature as Feature, codeql)); + + // set env var to false and check that the feature is now disabled + process.env[featureConfig[feature].envVar] = "false"; + t.assert(!(await features.getValue(feature as Feature, codeql))); + }); + }, + ); + } + + if (featureConfig[feature].toolsFeature !== undefined) { + test.serial( + `Feature '${feature}' is disabled if the required tools feature is not enabled`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + + const expectedFeatureEnablement = initializeFeatures(true); + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + + // feature should be disabled when the required tools feature is not enabled + let codeql = mockCodeQLVersion("2.0.0"); + t.assert(!(await features.getValue(feature as Feature, codeql))); + + // even setting the env var to true should not enable the feature if + // the required tools feature is not enabled + process.env[featureConfig[feature].envVar] = "true"; + t.assert(!(await features.getValue(feature as Feature, codeql))); + + // feature should be enabled when the required tools feature is enabled + // and env var is not set + process.env[featureConfig[feature].envVar] = ""; + codeql = mockCodeQLVersion("2.0.0", { + [featureConfig[feature].toolsFeature]: true, + }); + t.assert(await features.getValue(feature as Feature, codeql)); + + // set env var to false and check that the feature is now disabled + process.env[featureConfig[feature].envVar] = "false"; + t.assert(!(await features.getValue(feature as Feature, codeql))); + }); + }, + ); } } -test("Feature flags are saved to disk", async (t) => { +test.serial("Feature flags are saved to disk", async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); const expectedFeatureEnablement = initializeFeatures(true); @@ -376,38 +413,41 @@ test("Feature flags are saved to disk", async (t) => { }); }); -test("Environment variable can override feature flag cache", async (t) => { - await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(true); - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); +test.serial( + "Environment variable can override feature flag cache", + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + const expectedFeatureEnablement = initializeFeatures(true); + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - const cachedFeatureFlags = path.join(tmpDir, FEATURE_FLAGS_FILE_NAME); - t.true( - await getFeatureIncludingCodeQlIfRequired( - features, - Feature.QaTelemetryEnabled, - ), - "Feature flag should be enabled initially", - ); + const cachedFeatureFlags = path.join(tmpDir, FEATURE_FLAGS_FILE_NAME); + t.true( + await getFeatureIncludingCodeQlIfRequired( + features, + Feature.QaTelemetryEnabled, + ), + "Feature flag should be enabled initially", + ); - t.true( - fs.existsSync(cachedFeatureFlags), - "Feature flag cached file should exist after getting feature flags", - ); - process.env.CODEQL_ACTION_QA_TELEMETRY = "false"; + t.true( + fs.existsSync(cachedFeatureFlags), + "Feature flag cached file should exist after getting feature flags", + ); + process.env.CODEQL_ACTION_QA_TELEMETRY = "false"; - t.false( - await getFeatureIncludingCodeQlIfRequired( - features, - Feature.QaTelemetryEnabled, - ), - "Feature flag should be disabled after setting env var", - ); - }); -}); + t.false( + await getFeatureIncludingCodeQlIfRequired( + features, + Feature.QaTelemetryEnabled, + ), + "Feature flag should be disabled after setting env var", + ); + }); + }, +); -test(`selects CLI from defaults.json on GHES`, async (t) => { +test.serial(`selects CLI from defaults.json on GHES`, async (t) => { await withTmpDir(async (tmpDir) => { const features = setUpFeatureFlagTests(tmpDir); @@ -422,80 +462,94 @@ test(`selects CLI from defaults.json on GHES`, async (t) => { }); for (const variant of [GitHubVariant.DOTCOM, GitHubVariant.GHEC_DR]) { - test(`selects CLI v2.20.1 on ${variant} when feature flags enable v2.20.0 and v2.20.1`, async (t) => { - await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(true); - expectedFeatureEnablement["default_codeql_version_2_20_0_enabled"] = true; - expectedFeatureEnablement["default_codeql_version_2_20_1_enabled"] = true; - expectedFeatureEnablement["default_codeql_version_2_20_2_enabled"] = - false; - expectedFeatureEnablement["default_codeql_version_2_20_3_enabled"] = - false; - expectedFeatureEnablement["default_codeql_version_2_20_4_enabled"] = - false; - expectedFeatureEnablement["default_codeql_version_2_20_5_enabled"] = - false; - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + test.serial( + `selects CLI v2.20.1 on ${variant} when feature flags enable v2.20.0 and v2.20.1`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + const expectedFeatureEnablement = initializeFeatures(true); + expectedFeatureEnablement["default_codeql_version_2_20_0_enabled"] = + true; + expectedFeatureEnablement["default_codeql_version_2_20_1_enabled"] = + true; + expectedFeatureEnablement["default_codeql_version_2_20_2_enabled"] = + false; + expectedFeatureEnablement["default_codeql_version_2_20_3_enabled"] = + false; + expectedFeatureEnablement["default_codeql_version_2_20_4_enabled"] = + false; + expectedFeatureEnablement["default_codeql_version_2_20_5_enabled"] = + false; + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - const defaultCliVersion = await features.getDefaultCliVersion(variant); - t.deepEqual(defaultCliVersion, { - cliVersion: "2.20.1", - tagName: "codeql-bundle-v2.20.1", - toolsFeatureFlagsValid: true, + const defaultCliVersion = await features.getDefaultCliVersion(variant); + t.deepEqual(defaultCliVersion, { + cliVersion: "2.20.1", + tagName: "codeql-bundle-v2.20.1", + toolsFeatureFlagsValid: true, + }); }); - }); - }); + }, + ); - test(`selects CLI from defaults.json on ${variant} when no default version feature flags are enabled`, async (t) => { - await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); - const expectedFeatureEnablement = initializeFeatures(true); - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + test.serial( + `selects CLI from defaults.json on ${variant} when no default version feature flags are enabled`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + const expectedFeatureEnablement = initializeFeatures(true); + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - const defaultCliVersion = await features.getDefaultCliVersion(variant); - t.deepEqual(defaultCliVersion, { - cliVersion: defaults.cliVersion, - tagName: defaults.bundleVersion, - toolsFeatureFlagsValid: false, + const defaultCliVersion = await features.getDefaultCliVersion(variant); + t.deepEqual(defaultCliVersion, { + cliVersion: defaults.cliVersion, + tagName: defaults.bundleVersion, + toolsFeatureFlagsValid: false, + }); }); - }); - }); + }, + ); - test(`ignores invalid version numbers in default version feature flags on ${variant}`, async (t) => { - await withTmpDir(async (tmpDir) => { - const loggedMessages = []; - const features = setUpFeatureFlagTests( - tmpDir, - getRecordingLogger(loggedMessages), - ); - const expectedFeatureEnablement = initializeFeatures(true); - expectedFeatureEnablement["default_codeql_version_2_20_0_enabled"] = true; - expectedFeatureEnablement["default_codeql_version_2_20_1_enabled"] = true; - expectedFeatureEnablement["default_codeql_version_2_20_invalid_enabled"] = - true; - mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); + test.serial( + `ignores invalid version numbers in default version feature flags on ${variant}`, + async (t) => { + await withTmpDir(async (tmpDir) => { + const loggedMessages = []; + const features = setUpFeatureFlagTests( + tmpDir, + getRecordingLogger(loggedMessages), + ); + const expectedFeatureEnablement = initializeFeatures(true); + expectedFeatureEnablement["default_codeql_version_2_20_0_enabled"] = + true; + expectedFeatureEnablement["default_codeql_version_2_20_1_enabled"] = + true; + expectedFeatureEnablement[ + "default_codeql_version_2_20_invalid_enabled" + ] = true; + mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement); - const defaultCliVersion = await features.getDefaultCliVersion(variant); - t.deepEqual(defaultCliVersion, { - cliVersion: "2.20.1", - tagName: "codeql-bundle-v2.20.1", - toolsFeatureFlagsValid: true, - }); + const defaultCliVersion = await features.getDefaultCliVersion(variant); + t.deepEqual(defaultCliVersion, { + cliVersion: "2.20.1", + tagName: "codeql-bundle-v2.20.1", + toolsFeatureFlagsValid: true, + }); - t.assert( - loggedMessages.find( - (v: LoggedMessage) => - v.type === "warning" && - v.message === - "Ignoring feature flag default_codeql_version_2_20_invalid_enabled as it does not specify a valid CodeQL version.", - ) !== undefined, - ); - }); - }); + t.assert( + loggedMessages.find( + (v: LoggedMessage) => + v.type === "warning" && + v.message === + "Ignoring feature flag default_codeql_version_2_20_invalid_enabled as it does not specify a valid CodeQL version.", + ) !== undefined, + ); + }); + }, + ); } -test("legacy feature flags should end with _enabled", async (t) => { +test.serial("legacy feature flags should end with _enabled", async (t) => { for (const [feature, config] of Object.entries(featureConfig)) { if ((config satisfies FeatureConfig as FeatureConfig).legacyApi) { t.assert( @@ -506,31 +560,40 @@ test("legacy feature flags should end with _enabled", async (t) => { } }); -test("non-legacy feature flags should not end with _enabled", async (t) => { - for (const [feature, config] of Object.entries(featureConfig)) { - if (!(config satisfies FeatureConfig as FeatureConfig).legacyApi) { - t.false( - feature.endsWith("_enabled"), - `non-legacy feature ${feature} should not end with '_enabled'`, - ); +test.serial( + "non-legacy feature flags should not end with _enabled", + async (t) => { + for (const [feature, config] of Object.entries(featureConfig)) { + if (!(config satisfies FeatureConfig as FeatureConfig).legacyApi) { + t.false( + feature.endsWith("_enabled"), + `non-legacy feature ${feature} should not end with '_enabled'`, + ); + } } - } -}); - -test("non-legacy feature flags should not start with codeql_action_", async (t) => { - for (const [feature, config] of Object.entries(featureConfig)) { - if (!(config satisfies FeatureConfig as FeatureConfig).legacyApi) { - t.false( - feature.startsWith("codeql_action_"), - `non-legacy feature ${feature} should not start with 'codeql_action_'`, - ); + }, +); + +test.serial( + "non-legacy feature flags should not start with codeql_action_", + async (t) => { + for (const [feature, config] of Object.entries(featureConfig)) { + if (!(config satisfies FeatureConfig as FeatureConfig).legacyApi) { + t.false( + feature.startsWith("codeql_action_"), + `non-legacy feature ${feature} should not start with 'codeql_action_'`, + ); + } } - } -}); + }, +); -test("initFeatures returns a `Features` instance by default", async (t) => { - await withTmpDir(async (tmpDir) => { - const features = setUpFeatureFlagTests(tmpDir); - t.is("Features", features.constructor.name); - }); -}); +test.serial( + "initFeatures returns a `Features` instance by default", + async (t) => { + await withTmpDir(async (tmpDir) => { + const features = setUpFeatureFlagTests(tmpDir); + t.is("Features", features.constructor.name); + }); + }, +); diff --git a/src/feature-flags/properties.test.ts b/src/feature-flags/properties.test.ts index 8cf8ef7cde..afe9369325 100644 --- a/src/feature-flags/properties.test.ts +++ b/src/feature-flags/properties.test.ts @@ -11,76 +11,86 @@ import * as properties from "./properties"; setupTests(test); -test("loadPropertiesFromApi throws if response data is not an array", async (t) => { - sinon.stub(api, "getRepositoryProperties").resolves({ - headers: {}, - status: 200, - url: "", - data: {}, - }); - const logger = getRunnerLogger(true); - const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); - await t.throwsAsync( - properties.loadPropertiesFromApi( +test.serial( + "loadPropertiesFromApi throws if response data is not an array", + async (t) => { + sinon.stub(api, "getRepositoryProperties").resolves({ + headers: {}, + status: 200, + url: "", + data: {}, + }); + const logger = getRunnerLogger(true); + const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); + await t.throwsAsync( + properties.loadPropertiesFromApi( + { + type: util.GitHubVariant.DOTCOM, + }, + logger, + mockRepositoryNwo, + ), { - type: util.GitHubVariant.DOTCOM, + message: /Expected repository properties API to return an array/, }, - logger, - mockRepositoryNwo, - ), - { - message: /Expected repository properties API to return an array/, - }, - ); -}); + ); + }, +); -test("loadPropertiesFromApi throws if response data contains unexpected objects", async (t) => { - sinon.stub(api, "getRepositoryProperties").resolves({ - headers: {}, - status: 200, - url: "", - data: [{}], - }); - const logger = getRunnerLogger(true); - const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); - await t.throwsAsync( - properties.loadPropertiesFromApi( +test.serial( + "loadPropertiesFromApi throws if response data contains unexpected objects", + async (t) => { + sinon.stub(api, "getRepositoryProperties").resolves({ + headers: {}, + status: 200, + url: "", + data: [{}], + }); + const logger = getRunnerLogger(true); + const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); + await t.throwsAsync( + properties.loadPropertiesFromApi( + { + type: util.GitHubVariant.DOTCOM, + }, + logger, + mockRepositoryNwo, + ), { - type: util.GitHubVariant.DOTCOM, + message: + /Expected repository property object to have a 'property_name'/, + }, + ); + }, +); + +test.serial( + "loadPropertiesFromApi returns empty object if on GHES", + async (t) => { + sinon.stub(api, "getRepositoryProperties").resolves({ + headers: {}, + status: 200, + url: "", + data: [ + { property_name: "github-codeql-extra-queries", value: "+queries" }, + { property_name: "unknown-property", value: "something" }, + ] satisfies properties.GitHubPropertiesResponse, + }); + const logger = getRunnerLogger(true); + const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); + const response = await properties.loadPropertiesFromApi( + { + type: util.GitHubVariant.GHES, + version: "", }, logger, mockRepositoryNwo, - ), - { - message: /Expected repository property object to have a 'property_name'/, - }, - ); -}); - -test("loadPropertiesFromApi returns empty object if on GHES", async (t) => { - sinon.stub(api, "getRepositoryProperties").resolves({ - headers: {}, - status: 200, - url: "", - data: [ - { property_name: "github-codeql-extra-queries", value: "+queries" }, - { property_name: "unknown-property", value: "something" }, - ] satisfies properties.GitHubPropertiesResponse, - }); - const logger = getRunnerLogger(true); - const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); - const response = await properties.loadPropertiesFromApi( - { - type: util.GitHubVariant.GHES, - version: "", - }, - logger, - mockRepositoryNwo, - ); - t.deepEqual(response, {}); -}); + ); + t.deepEqual(response, {}); + }, +); -test("loadPropertiesFromApi loads known properties", async (t) => { +test.serial("loadPropertiesFromApi loads known properties", async (t) => { sinon.stub(api, "getRepositoryProperties").resolves({ headers: {}, status: 200, @@ -102,7 +112,7 @@ test("loadPropertiesFromApi loads known properties", async (t) => { t.deepEqual(response, { "github-codeql-extra-queries": "+queries" }); }); -test("loadPropertiesFromApi parses true boolean property", async (t) => { +test.serial("loadPropertiesFromApi parses true boolean property", async (t) => { sinon.stub(api, "getRepositoryProperties").resolves({ headers: {}, status: 200, @@ -132,86 +142,95 @@ test("loadPropertiesFromApi parses true boolean property", async (t) => { t.true(warningSpy.notCalled); }); -test("loadPropertiesFromApi parses false boolean property", async (t) => { - sinon.stub(api, "getRepositoryProperties").resolves({ - headers: {}, - status: 200, - url: "", - data: [ - { - property_name: "github-codeql-disable-overlay", - value: "false", - }, - ] satisfies properties.GitHubPropertiesResponse, - }); - const logger = getRunnerLogger(true); - const warningSpy = sinon.spy(logger, "warning"); - const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); - const response = await properties.loadPropertiesFromApi( - { - type: util.GitHubVariant.DOTCOM, - }, - logger, - mockRepositoryNwo, - ); - t.deepEqual(response, { - "github-codeql-disable-overlay": false, - }); - t.true(warningSpy.notCalled); -}); - -test("loadPropertiesFromApi throws if property value is not a string", async (t) => { - sinon.stub(api, "getRepositoryProperties").resolves({ - headers: {}, - status: 200, - url: "", - data: [{ property_name: "github-codeql-extra-queries", value: 123 }], - }); - const logger = getRunnerLogger(true); - const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); - await t.throwsAsync( - properties.loadPropertiesFromApi( +test.serial( + "loadPropertiesFromApi parses false boolean property", + async (t) => { + sinon.stub(api, "getRepositoryProperties").resolves({ + headers: {}, + status: 200, + url: "", + data: [ + { + property_name: "github-codeql-disable-overlay", + value: "false", + }, + ] satisfies properties.GitHubPropertiesResponse, + }); + const logger = getRunnerLogger(true); + const warningSpy = sinon.spy(logger, "warning"); + const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); + const response = await properties.loadPropertiesFromApi( { type: util.GitHubVariant.DOTCOM, }, logger, mockRepositoryNwo, - ), - { - message: - /Expected repository property 'github-codeql-extra-queries' to have a string value/, - }, - ); -}); + ); + t.deepEqual(response, { + "github-codeql-disable-overlay": false, + }); + t.true(warningSpy.notCalled); + }, +); -test("loadPropertiesFromApi warns if boolean property has unexpected value", async (t) => { - sinon.stub(api, "getRepositoryProperties").resolves({ - headers: {}, - status: 200, - url: "", - data: [ +test.serial( + "loadPropertiesFromApi throws if property value is not a string", + async (t) => { + sinon.stub(api, "getRepositoryProperties").resolves({ + headers: {}, + status: 200, + url: "", + data: [{ property_name: "github-codeql-extra-queries", value: 123 }], + }); + const logger = getRunnerLogger(true); + const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); + await t.throwsAsync( + properties.loadPropertiesFromApi( + { + type: util.GitHubVariant.DOTCOM, + }, + logger, + mockRepositoryNwo, + ), { - property_name: "github-codeql-disable-overlay", - value: "yes", + message: + /Expected repository property 'github-codeql-extra-queries' to have a string value/, }, - ] satisfies properties.GitHubPropertiesResponse, - }); - const logger = getRunnerLogger(true); - const warningSpy = sinon.spy(logger, "warning"); - const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); - const response = await properties.loadPropertiesFromApi( - { - type: util.GitHubVariant.DOTCOM, - }, - logger, - mockRepositoryNwo, - ); - t.deepEqual(response, { - "github-codeql-disable-overlay": false, - }); - t.true(warningSpy.calledOnce); - t.is( - warningSpy.firstCall.args[0], - "Repository property 'github-codeql-disable-overlay' has unexpected value 'yes'. Expected 'true' or 'false'. Defaulting to false.", - ); -}); + ); + }, +); + +test.serial( + "loadPropertiesFromApi warns if boolean property has unexpected value", + async (t) => { + sinon.stub(api, "getRepositoryProperties").resolves({ + headers: {}, + status: 200, + url: "", + data: [ + { + property_name: "github-codeql-disable-overlay", + value: "yes", + }, + ] satisfies properties.GitHubPropertiesResponse, + }); + const logger = getRunnerLogger(true); + const warningSpy = sinon.spy(logger, "warning"); + const mockRepositoryNwo = parseRepositoryNwo("owner/repo"); + const response = await properties.loadPropertiesFromApi( + { + type: util.GitHubVariant.DOTCOM, + }, + logger, + mockRepositoryNwo, + ); + t.deepEqual(response, { + "github-codeql-disable-overlay": false, + }); + t.true(warningSpy.calledOnce); + t.is( + warningSpy.firstCall.args[0], + "Repository property 'github-codeql-disable-overlay' has unexpected value 'yes'. Expected 'true' or 'false'. Defaulting to false.", + ); + }, +); diff --git a/src/git-utils.test.ts b/src/git-utils.test.ts index e4dbf84bc2..4c51bc1d9e 100644 --- a/src/git-utils.test.ts +++ b/src/git-utils.test.ts @@ -13,154 +13,187 @@ import { withTmpDir } from "./util"; setupTests(test); -test("getRef() throws on the empty string", async (t) => { +test.serial("getRef() throws on the empty string", async (t) => { process.env["GITHUB_REF"] = ""; await t.throwsAsync(gitUtils.getRef); }); -test("getRef() returns merge PR ref if GITHUB_SHA still checked out", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - const expectedRef = "refs/pull/1/merge"; - const currentSha = "a".repeat(40); - process.env["GITHUB_REF"] = expectedRef; - process.env["GITHUB_SHA"] = currentSha; - - const callback = sinon.stub(gitUtils, "getCommitOid"); - callback.withArgs("HEAD").resolves(currentSha); - - const actualRef = await gitUtils.getRef(); - t.deepEqual(actualRef, expectedRef); - callback.restore(); - }); -}); - -test("getRef() returns merge PR ref if GITHUB_REF still checked out but sha has changed (actions checkout@v1)", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - const expectedRef = "refs/pull/1/merge"; - process.env["GITHUB_REF"] = expectedRef; - process.env["GITHUB_SHA"] = "b".repeat(40); - const sha = "a".repeat(40); - - const callback = sinon.stub(gitUtils, "getCommitOid"); - callback.withArgs("refs/remotes/pull/1/merge").resolves(sha); - callback.withArgs("HEAD").resolves(sha); - - const actualRef = await gitUtils.getRef(); - t.deepEqual(actualRef, expectedRef); - callback.restore(); - }); -}); - -test("getRef() returns head PR ref if GITHUB_REF no longer checked out", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - process.env["GITHUB_REF"] = "refs/pull/1/merge"; - process.env["GITHUB_SHA"] = "a".repeat(40); - - const callback = sinon.stub(gitUtils, "getCommitOid"); - callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("a".repeat(40)); - callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40)); - - const actualRef = await gitUtils.getRef(); - t.deepEqual(actualRef, "refs/pull/1/head"); - callback.restore(); - }); -}); - -test("getRef() returns ref provided as an input and ignores current HEAD", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput"); - getAdditionalInputStub.withArgs("ref").resolves("refs/pull/2/merge"); - getAdditionalInputStub.withArgs("sha").resolves("b".repeat(40)); - - // These values are be ignored - process.env["GITHUB_REF"] = "refs/pull/1/merge"; - process.env["GITHUB_SHA"] = "a".repeat(40); - - const callback = sinon.stub(gitUtils, "getCommitOid"); - callback.withArgs("refs/pull/1/merge").resolves("b".repeat(40)); - callback.withArgs("HEAD").resolves("b".repeat(40)); - - const actualRef = await gitUtils.getRef(); - t.deepEqual(actualRef, "refs/pull/2/merge"); - callback.restore(); - getAdditionalInputStub.restore(); - }); -}); - -test("getRef() returns CODE_SCANNING_REF as a fallback for GITHUB_REF", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - const expectedRef = "refs/pull/1/HEAD"; - const currentSha = "a".repeat(40); - process.env["CODE_SCANNING_REF"] = expectedRef; - process.env["GITHUB_REF"] = ""; - process.env["GITHUB_SHA"] = currentSha; - - const actualRef = await gitUtils.getRef(); - t.deepEqual(actualRef, expectedRef); - }); -}); - -test("getRef() returns GITHUB_REF over CODE_SCANNING_REF if both are provided", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - const expectedRef = "refs/pull/1/merge"; - const currentSha = "a".repeat(40); - process.env["CODE_SCANNING_REF"] = "refs/pull/1/HEAD"; - process.env["GITHUB_REF"] = expectedRef; - process.env["GITHUB_SHA"] = currentSha; - - const actualRef = await gitUtils.getRef(); - t.deepEqual(actualRef, expectedRef); - }); -}); - -test("getRef() throws an error if only `ref` is provided as an input", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput"); - getAdditionalInputStub.withArgs("ref").resolves("refs/pull/1/merge"); - - await t.throwsAsync( - async () => { - await gitUtils.getRef(); - }, - { - instanceOf: Error, - message: - "Both 'ref' and 'sha' are required if one of them is provided.", - }, - ); - getAdditionalInputStub.restore(); - }); -}); - -test("getRef() throws an error if only `sha` is provided as an input", async (t) => { - await withTmpDir(async (tmpDir: string) => { - setupActionsVars(tmpDir, tmpDir); - process.env["GITHUB_WORKSPACE"] = "/tmp"; - const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput"); - getAdditionalInputStub.withArgs("sha").resolves("a".repeat(40)); - - await t.throwsAsync( - async () => { - await gitUtils.getRef(); - }, - { - instanceOf: Error, - message: - "Both 'ref' and 'sha' are required if one of them is provided.", - }, - ); - getAdditionalInputStub.restore(); - }); -}); +test.serial( + "getRef() returns merge PR ref if GITHUB_SHA still checked out", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + const expectedRef = "refs/pull/1/merge"; + const currentSha = "a".repeat(40); + process.env["GITHUB_REF"] = expectedRef; + process.env["GITHUB_SHA"] = currentSha; + + const callback = sinon.stub(gitUtils, "getCommitOid"); + callback.withArgs("HEAD").resolves(currentSha); + + const actualRef = await gitUtils.getRef(); + t.deepEqual(actualRef, expectedRef); + callback.restore(); + }); + }, +); + +test.serial( + "getRef() returns merge PR ref if GITHUB_REF still checked out but sha has changed (actions checkout@v1)", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + const expectedRef = "refs/pull/1/merge"; + process.env["GITHUB_REF"] = expectedRef; + process.env["GITHUB_SHA"] = "b".repeat(40); + const sha = "a".repeat(40); + + const callback = sinon.stub(gitUtils, "getCommitOid"); + callback.withArgs("refs/remotes/pull/1/merge").resolves(sha); + callback.withArgs("HEAD").resolves(sha); + + const actualRef = await gitUtils.getRef(); + t.deepEqual(actualRef, expectedRef); + callback.restore(); + }); + }, +); + +test.serial( + "getRef() returns head PR ref if GITHUB_REF no longer checked out", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + process.env["GITHUB_REF"] = "refs/pull/1/merge"; + process.env["GITHUB_SHA"] = "a".repeat(40); + + const callback = sinon.stub(gitUtils, "getCommitOid"); + callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("a".repeat(40)); + callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40)); + + const actualRef = await gitUtils.getRef(); + t.deepEqual(actualRef, "refs/pull/1/head"); + callback.restore(); + }); + }, +); + +test.serial( + "getRef() returns ref provided as an input and ignores current HEAD", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + const getAdditionalInputStub = sinon.stub( + actionsUtil, + "getOptionalInput", + ); + getAdditionalInputStub.withArgs("ref").resolves("refs/pull/2/merge"); + getAdditionalInputStub.withArgs("sha").resolves("b".repeat(40)); + + // These values are be ignored + process.env["GITHUB_REF"] = "refs/pull/1/merge"; + process.env["GITHUB_SHA"] = "a".repeat(40); + + const callback = sinon.stub(gitUtils, "getCommitOid"); + callback.withArgs("refs/pull/1/merge").resolves("b".repeat(40)); + callback.withArgs("HEAD").resolves("b".repeat(40)); + + const actualRef = await gitUtils.getRef(); + t.deepEqual(actualRef, "refs/pull/2/merge"); + callback.restore(); + getAdditionalInputStub.restore(); + }); + }, +); + +test.serial( + "getRef() returns CODE_SCANNING_REF as a fallback for GITHUB_REF", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + const expectedRef = "refs/pull/1/HEAD"; + const currentSha = "a".repeat(40); + process.env["CODE_SCANNING_REF"] = expectedRef; + process.env["GITHUB_REF"] = ""; + process.env["GITHUB_SHA"] = currentSha; + + const actualRef = await gitUtils.getRef(); + t.deepEqual(actualRef, expectedRef); + }); + }, +); + +test.serial( + "getRef() returns GITHUB_REF over CODE_SCANNING_REF if both are provided", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + const expectedRef = "refs/pull/1/merge"; + const currentSha = "a".repeat(40); + process.env["CODE_SCANNING_REF"] = "refs/pull/1/HEAD"; + process.env["GITHUB_REF"] = expectedRef; + process.env["GITHUB_SHA"] = currentSha; + + const actualRef = await gitUtils.getRef(); + t.deepEqual(actualRef, expectedRef); + }); + }, +); + +test.serial( + "getRef() throws an error if only `ref` is provided as an input", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + const getAdditionalInputStub = sinon.stub( + actionsUtil, + "getOptionalInput", + ); + getAdditionalInputStub.withArgs("ref").resolves("refs/pull/1/merge"); + + await t.throwsAsync( + async () => { + await gitUtils.getRef(); + }, + { + instanceOf: Error, + message: + "Both 'ref' and 'sha' are required if one of them is provided.", + }, + ); + getAdditionalInputStub.restore(); + }); + }, +); + +test.serial( + "getRef() throws an error if only `sha` is provided as an input", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupActionsVars(tmpDir, tmpDir); + process.env["GITHUB_WORKSPACE"] = "/tmp"; + const getAdditionalInputStub = sinon.stub( + actionsUtil, + "getOptionalInput", + ); + getAdditionalInputStub.withArgs("sha").resolves("a".repeat(40)); + + await t.throwsAsync( + async () => { + await gitUtils.getRef(); + }, + { + instanceOf: Error, + message: + "Both 'ref' and 'sha' are required if one of them is provided.", + }, + ); + getAdditionalInputStub.restore(); + }); + }, +); -test("isAnalyzingDefaultBranch()", async (t) => { +test.serial("isAnalyzingDefaultBranch()", async (t) => { process.env["GITHUB_EVENT_NAME"] = "push"; process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "true"; t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true); @@ -213,7 +246,7 @@ test("isAnalyzingDefaultBranch()", async (t) => { }); }); -test("determineBaseBranchHeadCommitOid non-pullrequest", async (t) => { +test.serial("determineBaseBranchHeadCommitOid non-pullrequest", async (t) => { const infoStub = sinon.stub(core, "info"); process.env["GITHUB_EVENT_NAME"] = "hucairz"; @@ -225,27 +258,30 @@ test("determineBaseBranchHeadCommitOid non-pullrequest", async (t) => { infoStub.restore(); }); -test("determineBaseBranchHeadCommitOid not git repository", async (t) => { - const infoStub = sinon.stub(core, "info"); +test.serial( + "determineBaseBranchHeadCommitOid not git repository", + async (t) => { + const infoStub = sinon.stub(core, "info"); - process.env["GITHUB_EVENT_NAME"] = "pull_request"; - process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a"; + process.env["GITHUB_EVENT_NAME"] = "pull_request"; + process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a"; - await withTmpDir(async (tmpDir) => { - await gitUtils.determineBaseBranchHeadCommitOid(tmpDir); - }); + await withTmpDir(async (tmpDir) => { + await gitUtils.determineBaseBranchHeadCommitOid(tmpDir); + }); - t.deepEqual(1, infoStub.callCount); - t.deepEqual( - infoStub.firstCall.args[0], - "git call failed. Will calculate the base branch SHA on the server. Error: " + - "The checkout path provided to the action does not appear to be a git repository.", - ); + t.deepEqual(1, infoStub.callCount); + t.deepEqual( + infoStub.firstCall.args[0], + "git call failed. Will calculate the base branch SHA on the server. Error: " + + "The checkout path provided to the action does not appear to be a git repository.", + ); - infoStub.restore(); -}); + infoStub.restore(); + }, +); -test("determineBaseBranchHeadCommitOid other error", async (t) => { +test.serial("determineBaseBranchHeadCommitOid other error", async (t) => { const infoStub = sinon.stub(core, "info"); process.env["GITHUB_EVENT_NAME"] = "pull_request"; @@ -269,7 +305,7 @@ test("determineBaseBranchHeadCommitOid other error", async (t) => { infoStub.restore(); }); -test("decodeGitFilePath unquoted strings", async (t) => { +test.serial("decodeGitFilePath unquoted strings", async (t) => { t.deepEqual(gitUtils.decodeGitFilePath("foo"), "foo"); t.deepEqual(gitUtils.decodeGitFilePath("foo bar"), "foo bar"); t.deepEqual(gitUtils.decodeGitFilePath("foo\\\\bar"), "foo\\\\bar"); @@ -288,7 +324,7 @@ test("decodeGitFilePath unquoted strings", async (t) => { ); }); -test("decodeGitFilePath quoted strings", async (t) => { +test.serial("decodeGitFilePath quoted strings", async (t) => { t.deepEqual(gitUtils.decodeGitFilePath('"foo"'), "foo"); t.deepEqual(gitUtils.decodeGitFilePath('"foo bar"'), "foo bar"); t.deepEqual(gitUtils.decodeGitFilePath('"foo\\\\bar"'), "foo\\bar"); @@ -307,7 +343,7 @@ test("decodeGitFilePath quoted strings", async (t) => { ); }); -test("getFileOidsUnderPath returns correct file mapping", async (t) => { +test.serial("getFileOidsUnderPath returns correct file mapping", async (t) => { const runGitCommandStub = sinon .stub(gitUtils as any, "runGitCommand") .resolves( @@ -331,7 +367,7 @@ test("getFileOidsUnderPath returns correct file mapping", async (t) => { ]); }); -test("getFileOidsUnderPath handles quoted paths", async (t) => { +test.serial("getFileOidsUnderPath handles quoted paths", async (t) => { sinon .stub(gitUtils as any, "runGitCommand") .resolves( @@ -349,44 +385,50 @@ test("getFileOidsUnderPath handles quoted paths", async (t) => { }); }); -test("getFileOidsUnderPath handles empty output", async (t) => { +test.serial("getFileOidsUnderPath handles empty output", async (t) => { sinon.stub(gitUtils as any, "runGitCommand").resolves(""); const result = await gitUtils.getFileOidsUnderPath("/fake/path"); t.deepEqual(result, {}); }); -test("getFileOidsUnderPath throws on unexpected output format", async (t) => { - sinon - .stub(gitUtils as any, "runGitCommand") - .resolves( - "30d998ded095371488be3a729eb61d86ed721a18_lib/git-utils.js\n" + - "invalid-line-format\n" + - "a47c11f5bfdca7661942d2c8f1b7209fb0dfdf96_src/git-utils.ts", - ); - - await t.throwsAsync( - async () => { - await gitUtils.getFileOidsUnderPath("/fake/path"); - }, - { - instanceOf: Error, - message: 'Unexpected "git ls-files" output: invalid-line-format', - }, - ); -}); - -test("getGitVersionOrThrow returns version for valid git output", async (t) => { - sinon - .stub(gitUtils as any, "runGitCommand") - .resolves(`git version 2.40.0${os.EOL}`); - - const version = await gitUtils.getGitVersionOrThrow(); - t.is(version.truncatedVersion, "2.40.0"); - t.is(version.fullVersion, "2.40.0"); -}); +test.serial( + "getFileOidsUnderPath throws on unexpected output format", + async (t) => { + sinon + .stub(gitUtils as any, "runGitCommand") + .resolves( + "30d998ded095371488be3a729eb61d86ed721a18_lib/git-utils.js\n" + + "invalid-line-format\n" + + "a47c11f5bfdca7661942d2c8f1b7209fb0dfdf96_src/git-utils.ts", + ); -test("getGitVersionOrThrow throws for invalid git output", async (t) => { + await t.throwsAsync( + async () => { + await gitUtils.getFileOidsUnderPath("/fake/path"); + }, + { + instanceOf: Error, + message: 'Unexpected "git ls-files" output: invalid-line-format', + }, + ); + }, +); + +test.serial( + "getGitVersionOrThrow returns version for valid git output", + async (t) => { + sinon + .stub(gitUtils as any, "runGitCommand") + .resolves(`git version 2.40.0${os.EOL}`); + + const version = await gitUtils.getGitVersionOrThrow(); + t.is(version.truncatedVersion, "2.40.0"); + t.is(version.fullVersion, "2.40.0"); + }, +); + +test.serial("getGitVersionOrThrow throws for invalid git output", async (t) => { sinon.stub(gitUtils as any, "runGitCommand").resolves("invalid output"); await t.throwsAsync( @@ -400,18 +442,21 @@ test("getGitVersionOrThrow throws for invalid git output", async (t) => { ); }); -test("getGitVersionOrThrow handles Windows-style git output", async (t) => { - sinon - .stub(gitUtils as any, "runGitCommand") - .resolves("git version 2.40.0.windows.1"); - - const version = await gitUtils.getGitVersionOrThrow(); - // The truncated version should contain just the major.minor.patch portion - t.is(version.truncatedVersion, "2.40.0"); - t.is(version.fullVersion, "2.40.0.windows.1"); -}); - -test("getGitVersionOrThrow throws when git command fails", async (t) => { +test.serial( + "getGitVersionOrThrow handles Windows-style git output", + async (t) => { + sinon + .stub(gitUtils as any, "runGitCommand") + .resolves("git version 2.40.0.windows.1"); + + const version = await gitUtils.getGitVersionOrThrow(); + // The truncated version should contain just the major.minor.patch portion + t.is(version.truncatedVersion, "2.40.0"); + t.is(version.fullVersion, "2.40.0.windows.1"); + }, +); + +test.serial("getGitVersionOrThrow throws when git command fails", async (t) => { sinon .stub(gitUtils as any, "runGitCommand") .rejects(new Error("git not found")); @@ -427,16 +472,19 @@ test("getGitVersionOrThrow throws when git command fails", async (t) => { ); }); -test("GitVersionInfo.isAtLeast correctly compares versions", async (t) => { - const version = new gitUtils.GitVersionInfo("2.40.0", "2.40.0"); +test.serial( + "GitVersionInfo.isAtLeast correctly compares versions", + async (t) => { + const version = new gitUtils.GitVersionInfo("2.40.0", "2.40.0"); - t.true(version.isAtLeast("2.38.0")); - t.true(version.isAtLeast("2.40.0")); - t.false(version.isAtLeast("2.41.0")); - t.false(version.isAtLeast("3.0.0")); -}); + t.true(version.isAtLeast("2.38.0")); + t.true(version.isAtLeast("2.40.0")); + t.false(version.isAtLeast("2.41.0")); + t.false(version.isAtLeast("3.0.0")); + }, +); -test("listFiles returns array of file paths", async (t) => { +test.serial("listFiles returns array of file paths", async (t) => { sinon .stub(gitUtils, "runGitCommand") .resolves(["dir/file.txt", "README.txt", ""].join(os.EOL)); @@ -448,7 +496,7 @@ test("listFiles returns array of file paths", async (t) => { }); }); -test("getGeneratedFiles returns generated files only", async (t) => { +test.serial("getGeneratedFiles returns generated files only", async (t) => { const runGitCommandStub = sinon.stub(gitUtils, "runGitCommand"); runGitCommandStub diff --git a/src/init-action-post-helper.test.ts b/src/init-action-post-helper.test.ts index ee18c8fcc7..e9b72332b6 100644 --- a/src/init-action-post-helper.test.ts +++ b/src/init-action-post-helper.test.ts @@ -28,7 +28,7 @@ const NUM_BYTES_PER_GIB = 1024 * 1024 * 1024; setupTests(test); -test("init-post action with debug mode off", async (t) => { +test.serial("init-post action with debug mode off", async (t) => { return await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); @@ -61,7 +61,7 @@ test("init-post action with debug mode off", async (t) => { }); }); -test("init-post action with debug mode on", async (t) => { +test.serial("init-post action with debug mode on", async (t) => { return await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); @@ -83,83 +83,94 @@ test("init-post action with debug mode on", async (t) => { }); }); -test("uploads failed SARIF run with `diagnostics export` if feature flag is off", async (t) => { - const actionsWorkflow = createTestWorkflow([ - { - name: "Checkout repository", - uses: "actions/checkout@v5", - }, - { - name: "Initialize CodeQL", - uses: "github/codeql-action/init@v4", - with: { - languages: "javascript", +test.serial( + "uploads failed SARIF run with `diagnostics export` if feature flag is off", + async (t) => { + const actionsWorkflow = createTestWorkflow([ + { + name: "Checkout repository", + uses: "actions/checkout@v5", }, - }, - { - name: "Perform CodeQL Analysis", - uses: "github/codeql-action/analyze@v4", - with: { - category: "my-category", + { + name: "Initialize CodeQL", + uses: "github/codeql-action/init@v4", + with: { + languages: "javascript", + }, }, - }, - ]); - await testFailedSarifUpload(t, actionsWorkflow, { category: "my-category" }); -}); + { + name: "Perform CodeQL Analysis", + uses: "github/codeql-action/analyze@v4", + with: { + category: "my-category", + }, + }, + ]); + await testFailedSarifUpload(t, actionsWorkflow, { + category: "my-category", + }); + }, +); -test("uploads failed SARIF run with `diagnostics export` if the database doesn't exist", async (t) => { - const actionsWorkflow = createTestWorkflow([ - { - name: "Checkout repository", - uses: "actions/checkout@v5", - }, - { - name: "Initialize CodeQL", - uses: "github/codeql-action/init@v4", - with: { - languages: "javascript", +test.serial( + "uploads failed SARIF run with `diagnostics export` if the database doesn't exist", + async (t) => { + const actionsWorkflow = createTestWorkflow([ + { + name: "Checkout repository", + uses: "actions/checkout@v5", }, - }, - { - name: "Perform CodeQL Analysis", - uses: "github/codeql-action/analyze@v4", - with: { - category: "my-category", + { + name: "Initialize CodeQL", + uses: "github/codeql-action/init@v4", + with: { + languages: "javascript", + }, }, - }, - ]); - await testFailedSarifUpload(t, actionsWorkflow, { - category: "my-category", - databaseExists: false, - }); -}); + { + name: "Perform CodeQL Analysis", + uses: "github/codeql-action/analyze@v4", + with: { + category: "my-category", + }, + }, + ]); + await testFailedSarifUpload(t, actionsWorkflow, { + category: "my-category", + databaseExists: false, + }); + }, +); -test("uploads failed SARIF run with database export-diagnostics if the database exists and feature flag is on", async (t) => { - const actionsWorkflow = createTestWorkflow([ - { - name: "Checkout repository", - uses: "actions/checkout@v5", - }, - { - name: "Initialize CodeQL", - uses: "github/codeql-action/init@v4", - with: { - languages: "javascript", +test.serial( + "uploads failed SARIF run with database export-diagnostics if the database exists and feature flag is on", + async (t) => { + const actionsWorkflow = createTestWorkflow([ + { + name: "Checkout repository", + uses: "actions/checkout@v5", }, - }, - { - name: "Perform CodeQL Analysis", - uses: "github/codeql-action/analyze@v4", - with: { - category: "my-category", + { + name: "Initialize CodeQL", + uses: "github/codeql-action/init@v4", + with: { + languages: "javascript", + }, }, - }, - ]); - await testFailedSarifUpload(t, actionsWorkflow, { - category: "my-category", - exportDiagnosticsEnabled: true, - }); -}); + { + name: "Perform CodeQL Analysis", + uses: "github/codeql-action/analyze@v4", + with: { + category: "my-category", + }, + }, + ]); + await testFailedSarifUpload(t, actionsWorkflow, { + category: "my-category", + exportDiagnosticsEnabled: true, + }); + }, +); const UPLOAD_INPUT_TEST_CASES = [ { @@ -189,9 +200,49 @@ const UPLOAD_INPUT_TEST_CASES = [ ]; for (const { uploadInput, shouldUpload } of UPLOAD_INPUT_TEST_CASES) { - test(`does ${ - shouldUpload ? "" : "not " - }upload failed SARIF run for workflow with upload: ${uploadInput}`, async (t) => { + test.serial( + `does ${ + shouldUpload ? "" : "not " + }upload failed SARIF run for workflow with upload: ${uploadInput}`, + async (t) => { + const actionsWorkflow = createTestWorkflow([ + { + name: "Checkout repository", + uses: "actions/checkout@v5", + }, + { + name: "Initialize CodeQL", + uses: "github/codeql-action/init@v4", + with: { + languages: "javascript", + }, + }, + { + name: "Perform CodeQL Analysis", + uses: "github/codeql-action/analyze@v4", + with: { + category: "my-category", + upload: uploadInput, + }, + }, + ]); + const result = await testFailedSarifUpload(t, actionsWorkflow, { + category: "my-category", + expectUpload: shouldUpload, + }); + if (!shouldUpload) { + t.is( + result.upload_failed_run_skipped_because, + "SARIF upload is disabled", + ); + } + }, + ); +} + +test.serial( + "uploading failed SARIF run succeeds when workflow uses an input with a matrix var", + async (t) => { const actionsWorkflow = createTestWorkflow([ { name: "Checkout repository", @@ -208,216 +259,197 @@ for (const { uploadInput, shouldUpload } of UPLOAD_INPUT_TEST_CASES) { name: "Perform CodeQL Analysis", uses: "github/codeql-action/analyze@v4", with: { - category: "my-category", - upload: uploadInput, + category: "/language:${{ matrix.language }}", }, }, ]); - const result = await testFailedSarifUpload(t, actionsWorkflow, { - category: "my-category", - expectUpload: shouldUpload, + await testFailedSarifUpload(t, actionsWorkflow, { + category: "/language:csharp", + matrix: { language: "csharp" }, }); - if (!shouldUpload) { - t.is( - result.upload_failed_run_skipped_because, - "SARIF upload is disabled", - ); - } - }); -} + }, +); -test("uploading failed SARIF run succeeds when workflow uses an input with a matrix var", async (t) => { - const actionsWorkflow = createTestWorkflow([ - { - name: "Checkout repository", - uses: "actions/checkout@v5", - }, - { - name: "Initialize CodeQL", - uses: "github/codeql-action/init@v4", - with: { - languages: "javascript", - }, - }, - { - name: "Perform CodeQL Analysis", - uses: "github/codeql-action/analyze@v4", - with: { - category: "/language:${{ matrix.language }}", +test.serial( + "uploading failed SARIF run fails when workflow uses a complex upload input", + async (t) => { + const actionsWorkflow = createTestWorkflow([ + { + name: "Checkout repository", + uses: "actions/checkout@v5", }, - }, - ]); - await testFailedSarifUpload(t, actionsWorkflow, { - category: "/language:csharp", - matrix: { language: "csharp" }, - }); -}); - -test("uploading failed SARIF run fails when workflow uses a complex upload input", async (t) => { - const actionsWorkflow = createTestWorkflow([ - { - name: "Checkout repository", - uses: "actions/checkout@v5", - }, - { - name: "Initialize CodeQL", - uses: "github/codeql-action/init@v4", - with: { - languages: "javascript", + { + name: "Initialize CodeQL", + uses: "github/codeql-action/init@v4", + with: { + languages: "javascript", + }, }, - }, - { - name: "Perform CodeQL Analysis", - uses: "github/codeql-action/analyze@v4", - with: { - upload: "${{ matrix.language != 'csharp' }}", + { + name: "Perform CodeQL Analysis", + uses: "github/codeql-action/analyze@v4", + with: { + upload: "${{ matrix.language != 'csharp' }}", + }, }, - }, - ]); - const result = await testFailedSarifUpload(t, actionsWorkflow, { - expectUpload: false, - }); - t.is( - result.upload_failed_run_error, - "Could not get upload input to github/codeql-action/analyze since it contained an " + - "unrecognized dynamic value.", - ); -}); - -test("uploading failed SARIF run fails when workflow does not reference github/codeql-action", async (t) => { - const actionsWorkflow = createTestWorkflow([ - { - name: "Checkout repository", - uses: "actions/checkout@v5", - }, - ]); - const result = await testFailedSarifUpload(t, actionsWorkflow, { - expectUpload: false, - }); - t.is( - result.upload_failed_run_error, - "Could not get upload input to github/codeql-action/analyze since the analyze job does not " + - "call github/codeql-action/analyze.", - ); - t.truthy(result.upload_failed_run_stack_trace); -}); - -test("not uploading failed SARIF when `code-scanning` is not an enabled analysis kind", async (t) => { - const result = await testFailedSarifUpload(t, createTestWorkflow([]), { - analysisKinds: [AnalysisKind.CodeQuality], - expectUpload: false, - }); - t.is( - result.upload_failed_run_skipped_because, - "Code Scanning is not enabled.", - ); -}); - -test("saves overlay status when overlay-base analysis did not complete successfully", async (t) => { - return await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - // Ensure analyze did not complete successfully. - delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; - - const diskUsage: util.DiskUsage = { - numAvailableBytes: 100 * NUM_BYTES_PER_GIB, - numTotalBytes: 200 * NUM_BYTES_PER_GIB, - }; - sinon.stub(util, "checkDiskUsage").resolves(diskUsage); - - const saveOverlayStatusStub = sinon - .stub(overlayStatus, "saveOverlayStatus") - .resolves(true); - - const stubCodeQL = codeql.createStubCodeQL({}); - - await initActionPostHelper.run( - sinon.spy(), - sinon.spy(), - stubCodeQL, - createTestConfig({ - debugMode: false, - languages: ["javascript"], - overlayDatabaseMode: OverlayDatabaseMode.OverlayBase, - }), - parseRepositoryNwo("github/codeql-action"), - createFeatures([Feature.OverlayAnalysisStatusSave]), - getRunnerLogger(true), + ]); + const result = await testFailedSarifUpload(t, actionsWorkflow, { + expectUpload: false, + }); + t.is( + result.upload_failed_run_error, + "Could not get upload input to github/codeql-action/analyze since it contained an " + + "unrecognized dynamic value.", ); + }, +); - t.true( - saveOverlayStatusStub.calledOnce, - "saveOverlayStatus should be called exactly once", - ); - t.deepEqual( - saveOverlayStatusStub.firstCall.args[0], - stubCodeQL, - "first arg should be the CodeQL instance", - ); - t.deepEqual( - saveOverlayStatusStub.firstCall.args[1], - ["javascript"], - "second arg should be the languages", - ); - t.deepEqual( - saveOverlayStatusStub.firstCall.args[2], - diskUsage, - "third arg should be the disk usage", - ); - t.deepEqual( - saveOverlayStatusStub.firstCall.args[3], +test.serial( + "uploading failed SARIF run fails when workflow does not reference github/codeql-action", + async (t) => { + const actionsWorkflow = createTestWorkflow([ { - attemptedToBuildOverlayBaseDatabase: true, - builtOverlayBaseDatabase: false, - job: { - checkRunId: undefined, - workflowRunId: Number(DEFAULT_ACTIONS_VARS.GITHUB_RUN_ID), - workflowRunAttempt: Number(DEFAULT_ACTIONS_VARS.GITHUB_RUN_ATTEMPT), - name: DEFAULT_ACTIONS_VARS.GITHUB_JOB, - }, + name: "Checkout repository", + uses: "actions/checkout@v5", }, - "fourth arg should be the overlay status recording an unsuccessful build attempt with job details", + ]); + const result = await testFailedSarifUpload(t, actionsWorkflow, { + expectUpload: false, + }); + t.is( + result.upload_failed_run_error, + "Could not get upload input to github/codeql-action/analyze since the analyze job does not " + + "call github/codeql-action/analyze.", ); - }); -}); - -test("does not save overlay status when OverlayAnalysisStatusSave feature flag is disabled", async (t) => { - return await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - // Ensure analyze did not complete successfully. - delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; - - sinon.stub(util, "checkDiskUsage").resolves({ - numAvailableBytes: 100 * NUM_BYTES_PER_GIB, - numTotalBytes: 200 * NUM_BYTES_PER_GIB, + t.truthy(result.upload_failed_run_stack_trace); + }, +); + +test.serial( + "not uploading failed SARIF when `code-scanning` is not an enabled analysis kind", + async (t) => { + const result = await testFailedSarifUpload(t, createTestWorkflow([]), { + analysisKinds: [AnalysisKind.CodeQuality], + expectUpload: false, }); - - const saveOverlayStatusStub = sinon - .stub(overlayStatus, "saveOverlayStatus") - .resolves(true); - - await initActionPostHelper.run( - sinon.spy(), - sinon.spy(), - codeql.createStubCodeQL({}), - createTestConfig({ - debugMode: false, - languages: ["javascript"], - overlayDatabaseMode: OverlayDatabaseMode.OverlayBase, - }), - parseRepositoryNwo("github/codeql-action"), - createFeatures([]), - getRunnerLogger(true), + t.is( + result.upload_failed_run_skipped_because, + "Code Scanning is not enabled.", ); + }, +); + +test.serial( + "saves overlay status when overlay-base analysis did not complete successfully", + async (t) => { + return await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + // Ensure analyze did not complete successfully. + delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; + + const diskUsage: util.DiskUsage = { + numAvailableBytes: 100 * NUM_BYTES_PER_GIB, + numTotalBytes: 200 * NUM_BYTES_PER_GIB, + }; + sinon.stub(util, "checkDiskUsage").resolves(diskUsage); + + const saveOverlayStatusStub = sinon + .stub(overlayStatus, "saveOverlayStatus") + .resolves(true); + + const stubCodeQL = codeql.createStubCodeQL({}); + + await initActionPostHelper.run( + sinon.spy(), + sinon.spy(), + stubCodeQL, + createTestConfig({ + debugMode: false, + languages: ["javascript"], + overlayDatabaseMode: OverlayDatabaseMode.OverlayBase, + }), + parseRepositoryNwo("github/codeql-action"), + createFeatures([Feature.OverlayAnalysisStatusSave]), + getRunnerLogger(true), + ); - t.true( - saveOverlayStatusStub.notCalled, - "saveOverlayStatus should not be called when OverlayAnalysisStatusSave feature flag is disabled", - ); - }); -}); + t.true( + saveOverlayStatusStub.calledOnce, + "saveOverlayStatus should be called exactly once", + ); + t.deepEqual( + saveOverlayStatusStub.firstCall.args[0], + stubCodeQL, + "first arg should be the CodeQL instance", + ); + t.deepEqual( + saveOverlayStatusStub.firstCall.args[1], + ["javascript"], + "second arg should be the languages", + ); + t.deepEqual( + saveOverlayStatusStub.firstCall.args[2], + diskUsage, + "third arg should be the disk usage", + ); + t.deepEqual( + saveOverlayStatusStub.firstCall.args[3], + { + attemptedToBuildOverlayBaseDatabase: true, + builtOverlayBaseDatabase: false, + job: { + checkRunId: undefined, + workflowRunId: Number(DEFAULT_ACTIONS_VARS.GITHUB_RUN_ID), + workflowRunAttempt: Number(DEFAULT_ACTIONS_VARS.GITHUB_RUN_ATTEMPT), + name: DEFAULT_ACTIONS_VARS.GITHUB_JOB, + }, + }, + "fourth arg should be the overlay status recording an unsuccessful build attempt with job details", + ); + }); + }, +); + +test.serial( + "does not save overlay status when OverlayAnalysisStatusSave feature flag is disabled", + async (t) => { + return await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + // Ensure analyze did not complete successfully. + delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; + + sinon.stub(util, "checkDiskUsage").resolves({ + numAvailableBytes: 100 * NUM_BYTES_PER_GIB, + numTotalBytes: 200 * NUM_BYTES_PER_GIB, + }); + + const saveOverlayStatusStub = sinon + .stub(overlayStatus, "saveOverlayStatus") + .resolves(true); + + await initActionPostHelper.run( + sinon.spy(), + sinon.spy(), + codeql.createStubCodeQL({}), + createTestConfig({ + debugMode: false, + languages: ["javascript"], + overlayDatabaseMode: OverlayDatabaseMode.OverlayBase, + }), + parseRepositoryNwo("github/codeql-action"), + createFeatures([]), + getRunnerLogger(true), + ); + + t.true( + saveOverlayStatusStub.notCalled, + "saveOverlayStatus should not be called when OverlayAnalysisStatusSave feature flag is disabled", + ); + }); + }, +); -test("does not save overlay status when build successful", async (t) => { +test.serial("does not save overlay status when build successful", async (t) => { return await util.withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); // Mark analyze as having completed successfully. @@ -453,40 +485,43 @@ test("does not save overlay status when build successful", async (t) => { }); }); -test("does not save overlay status when overlay not enabled", async (t) => { - return await util.withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; +test.serial( + "does not save overlay status when overlay not enabled", + async (t) => { + return await util.withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]; + + sinon.stub(util, "checkDiskUsage").resolves({ + numAvailableBytes: 100 * NUM_BYTES_PER_GIB, + numTotalBytes: 200 * NUM_BYTES_PER_GIB, + }); + + const saveOverlayStatusStub = sinon + .stub(overlayStatus, "saveOverlayStatus") + .resolves(true); + + await initActionPostHelper.run( + sinon.spy(), + sinon.spy(), + codeql.createStubCodeQL({}), + createTestConfig({ + debugMode: false, + languages: ["javascript"], + overlayDatabaseMode: OverlayDatabaseMode.None, + }), + parseRepositoryNwo("github/codeql-action"), + createFeatures([]), + getRunnerLogger(true), + ); - sinon.stub(util, "checkDiskUsage").resolves({ - numAvailableBytes: 100 * NUM_BYTES_PER_GIB, - numTotalBytes: 200 * NUM_BYTES_PER_GIB, + t.true( + saveOverlayStatusStub.notCalled, + "saveOverlayStatus should not be called when overlay is not enabled", + ); }); - - const saveOverlayStatusStub = sinon - .stub(overlayStatus, "saveOverlayStatus") - .resolves(true); - - await initActionPostHelper.run( - sinon.spy(), - sinon.spy(), - codeql.createStubCodeQL({}), - createTestConfig({ - debugMode: false, - languages: ["javascript"], - overlayDatabaseMode: OverlayDatabaseMode.None, - }), - parseRepositoryNwo("github/codeql-action"), - createFeatures([]), - getRunnerLogger(true), - ); - - t.true( - saveOverlayStatusStub.notCalled, - "saveOverlayStatus should not be called when overlay is not enabled", - ); - }); -}); + }, +); function createTestWorkflow( steps: workflow.WorkflowJobStep[], diff --git a/src/init.test.ts b/src/init.test.ts index 8106a78f9a..a7d4f4de17 100644 --- a/src/init.test.ts +++ b/src/init.test.ts @@ -77,46 +77,49 @@ for (const { runnerEnv, ErrorConstructor, message } of [ "otherwise we recommend rerunning the job.", }, ]) { - test(`cleanupDatabaseClusterDirectory throws a ${ErrorConstructor.name} when cleanup fails on ${runnerEnv} runner`, async (t) => { - await withTmpDir(async (tmpDir: string) => { - process.env["RUNNER_ENVIRONMENT"] = runnerEnv; - - const dbLocation = path.resolve(tmpDir, "dbs"); - fs.mkdirSync(dbLocation, { recursive: true }); - - const fileToCleanUp = path.resolve( - dbLocation, - "something-to-cleanup.txt", - ); - fs.writeFileSync(fileToCleanUp, ""); - - const rmSyncError = `Failed to clean up file ${fileToCleanUp}`; - - const messages: LoggedMessage[] = []; - t.throws( - () => - cleanupDatabaseClusterDirectory( - createTestConfig({ dbLocation }), - getRecordingLogger(messages), - {}, - () => { - throw new Error(rmSyncError); - }, - ), - { - instanceOf: ErrorConstructor, - message: `${message(dbLocation)} Details: ${rmSyncError}`, - }, - ); + test.serial( + `cleanupDatabaseClusterDirectory throws a ${ErrorConstructor.name} when cleanup fails on ${runnerEnv} runner`, + async (t) => { + await withTmpDir(async (tmpDir: string) => { + process.env["RUNNER_ENVIRONMENT"] = runnerEnv; + + const dbLocation = path.resolve(tmpDir, "dbs"); + fs.mkdirSync(dbLocation, { recursive: true }); + + const fileToCleanUp = path.resolve( + dbLocation, + "something-to-cleanup.txt", + ); + fs.writeFileSync(fileToCleanUp, ""); + + const rmSyncError = `Failed to clean up file ${fileToCleanUp}`; + + const messages: LoggedMessage[] = []; + t.throws( + () => + cleanupDatabaseClusterDirectory( + createTestConfig({ dbLocation }), + getRecordingLogger(messages), + {}, + () => { + throw new Error(rmSyncError); + }, + ), + { + instanceOf: ErrorConstructor, + message: `${message(dbLocation)} Details: ${rmSyncError}`, + }, + ); - t.is(messages.length, 1); - t.is(messages[0].type, "warning"); - t.is( - messages[0].message, - `The database cluster directory ${dbLocation} must be empty. Attempting to clean it up.`, - ); - }); - }); + t.is(messages.length, 1); + t.is(messages[0].type, "warning"); + t.is( + messages[0].message, + `The database cluster directory ${dbLocation} must be empty. Attempting to clean it up.`, + ); + }); + }, + ); } test("cleanupDatabaseClusterDirectory can disable warning with options", async (t) => { @@ -459,50 +462,62 @@ test("file coverage information enabled when debugMode is true", async (t) => { ); }); -test("file coverage information enabled when not analyzing a pull request", async (t) => { - sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(false); - - t.true( - await getFileCoverageInformationEnabled( - false, // debugMode - parseRepositoryNwo("github/codeql-action"), - createFeatures([Feature.SkipFileCoverageOnPrs]), - ), - ); -}); - -test("file coverage information enabled when owner is not 'github'", async (t) => { - sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true); - - t.true( - await getFileCoverageInformationEnabled( - false, // debugMode - parseRepositoryNwo("other-org/some-repo"), - createFeatures([Feature.SkipFileCoverageOnPrs]), - ), - ); -}); - -test("file coverage information enabled when feature flag is not enabled", async (t) => { - sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true); +test.serial( + "file coverage information enabled when not analyzing a pull request", + async (t) => { + sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(false); + + t.true( + await getFileCoverageInformationEnabled( + false, // debugMode + parseRepositoryNwo("github/codeql-action"), + createFeatures([Feature.SkipFileCoverageOnPrs]), + ), + ); + }, +); - t.true( - await getFileCoverageInformationEnabled( - false, // debugMode - parseRepositoryNwo("github/codeql-action"), - createFeatures([]), - ), - ); -}); +test.serial( + "file coverage information enabled when owner is not 'github'", + async (t) => { + sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true); + + t.true( + await getFileCoverageInformationEnabled( + false, // debugMode + parseRepositoryNwo("other-org/some-repo"), + createFeatures([Feature.SkipFileCoverageOnPrs]), + ), + ); + }, +); -test("file coverage information disabled when all conditions for skipping are met", async (t) => { - sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true); +test.serial( + "file coverage information enabled when feature flag is not enabled", + async (t) => { + sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true); + + t.true( + await getFileCoverageInformationEnabled( + false, // debugMode + parseRepositoryNwo("github/codeql-action"), + createFeatures([]), + ), + ); + }, +); - t.false( - await getFileCoverageInformationEnabled( - false, // debugMode - parseRepositoryNwo("github/codeql-action"), - createFeatures([Feature.SkipFileCoverageOnPrs]), - ), - ); -}); +test.serial( + "file coverage information disabled when all conditions for skipping are met", + async (t) => { + sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true); + + t.false( + await getFileCoverageInformationEnabled( + false, // debugMode + parseRepositoryNwo("github/codeql-action"), + createFeatures([Feature.SkipFileCoverageOnPrs]), + ), + ); + }, +); diff --git a/src/overlay/index.test.ts b/src/overlay/index.test.ts index 7e63520f5b..8e92a69e27 100644 --- a/src/overlay/index.test.ts +++ b/src/overlay/index.test.ts @@ -30,65 +30,68 @@ import { setupTests(test); -test("writeOverlayChangesFile generates correct changes file", async (t) => { - await withTmpDir(async (tmpDir) => { - const dbLocation = path.join(tmpDir, "db"); - await fs.promises.mkdir(dbLocation, { recursive: true }); - const sourceRoot = path.join(tmpDir, "src"); - await fs.promises.mkdir(sourceRoot, { recursive: true }); - const tempDir = path.join(tmpDir, "temp"); - await fs.promises.mkdir(tempDir, { recursive: true }); - - const logger = getRunnerLogger(true); - const config = createTestConfig({ dbLocation }); - - // Mock the getFileOidsUnderPath function to return base OIDs - const baseOids = { - "unchanged.js": "aaa111", - "modified.js": "bbb222", - "deleted.js": "ccc333", - }; - const getFileOidsStubForBase = sinon - .stub(gitUtils, "getFileOidsUnderPath") - .resolves(baseOids); - - // Write the base database OIDs file - await writeBaseDatabaseOidsFile(config, sourceRoot); - getFileOidsStubForBase.restore(); - - // Mock the getFileOidsUnderPath function to return overlay OIDs - const currentOids = { - "unchanged.js": "aaa111", - "modified.js": "ddd444", // Changed OID - "added.js": "eee555", // New file - }; - const getFileOidsStubForOverlay = sinon - .stub(gitUtils, "getFileOidsUnderPath") - .resolves(currentOids); - - // Write the overlay changes file, which uses the mocked overlay OIDs - // and the base database OIDs file - const getTempDirStub = sinon - .stub(actionsUtil, "getTemporaryDirectory") - .returns(tempDir); - const changesFilePath = await writeOverlayChangesFile( - config, - sourceRoot, - logger, - ); - getFileOidsStubForOverlay.restore(); - getTempDirStub.restore(); - - const fileContent = await fs.promises.readFile(changesFilePath, "utf-8"); - const parsedContent = JSON.parse(fileContent) as { changes: string[] }; - - t.deepEqual( - parsedContent.changes.sort(), - ["added.js", "deleted.js", "modified.js"], - "Should identify added, deleted, and modified files", - ); - }); -}); +test.serial( + "writeOverlayChangesFile generates correct changes file", + async (t) => { + await withTmpDir(async (tmpDir) => { + const dbLocation = path.join(tmpDir, "db"); + await fs.promises.mkdir(dbLocation, { recursive: true }); + const sourceRoot = path.join(tmpDir, "src"); + await fs.promises.mkdir(sourceRoot, { recursive: true }); + const tempDir = path.join(tmpDir, "temp"); + await fs.promises.mkdir(tempDir, { recursive: true }); + + const logger = getRunnerLogger(true); + const config = createTestConfig({ dbLocation }); + + // Mock the getFileOidsUnderPath function to return base OIDs + const baseOids = { + "unchanged.js": "aaa111", + "modified.js": "bbb222", + "deleted.js": "ccc333", + }; + const getFileOidsStubForBase = sinon + .stub(gitUtils, "getFileOidsUnderPath") + .resolves(baseOids); + + // Write the base database OIDs file + await writeBaseDatabaseOidsFile(config, sourceRoot); + getFileOidsStubForBase.restore(); + + // Mock the getFileOidsUnderPath function to return overlay OIDs + const currentOids = { + "unchanged.js": "aaa111", + "modified.js": "ddd444", // Changed OID + "added.js": "eee555", // New file + }; + const getFileOidsStubForOverlay = sinon + .stub(gitUtils, "getFileOidsUnderPath") + .resolves(currentOids); + + // Write the overlay changes file, which uses the mocked overlay OIDs + // and the base database OIDs file + const getTempDirStub = sinon + .stub(actionsUtil, "getTemporaryDirectory") + .returns(tempDir); + const changesFilePath = await writeOverlayChangesFile( + config, + sourceRoot, + logger, + ); + getFileOidsStubForOverlay.restore(); + getTempDirStub.restore(); + + const fileContent = await fs.promises.readFile(changesFilePath, "utf-8"); + const parsedContent = JSON.parse(fileContent) as { changes: string[] }; + + t.deepEqual( + parsedContent.changes.sort(), + ["added.js", "deleted.js", "modified.js"], + "Should identify added, deleted, and modified files", + ); + }); + }, +); interface DownloadOverlayBaseDatabaseTestCase { overlayDatabaseMode: OverlayDatabaseMode; @@ -206,14 +209,14 @@ const testDownloadOverlayBaseDatabaseFromCache = test.macro({ title: (_, title) => `downloadOverlayBaseDatabaseFromCache: ${title}`, }); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns stats when successful", {}, true, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when mode is OverlayDatabaseMode.OverlayBase", { @@ -222,7 +225,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when mode is OverlayDatabaseMode.None", { @@ -231,7 +234,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when caching is disabled", { @@ -240,7 +243,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined in test mode", { @@ -249,7 +252,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when cache miss", { @@ -258,7 +261,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when download fails", { @@ -267,7 +270,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when downloaded database is invalid", { @@ -276,7 +279,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when downloaded database doesn't have an overlayBaseSpecifier", { @@ -285,7 +288,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when resolving database metadata fails", { @@ -294,7 +297,7 @@ test( false, ); -test( +test.serial( testDownloadOverlayBaseDatabaseFromCache, "returns undefined when filesystem error occurs", { @@ -303,7 +306,7 @@ test( false, ); -test("overlay-base database cache keys remain stable", async (t) => { +test.serial("overlay-base database cache keys remain stable", async (t) => { const logger = getRunnerLogger(true); const config = createTestConfig({ languages: ["python", "javascript"] }); const codeQlVersion = "2.23.0"; diff --git a/src/overlay/status.test.ts b/src/overlay/status.test.ts index 066b963b8c..d9fa48d90b 100644 --- a/src/overlay/status.test.ts +++ b/src/overlay/status.test.ts @@ -72,101 +72,110 @@ test("getCacheKey rounds disk space down to nearest 10 GiB", async (t) => { ); }); -test("shouldSkipOverlayAnalysis returns false when no cached status exists", async (t) => { - await withTmpDir(async (tmpDir) => { - process.env["RUNNER_TEMP"] = tmpDir; - const codeql = mockCodeQLVersion("2.20.0"); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - - sinon.stub(actionsCache, "restoreCache").resolves(undefined); - - const result = await shouldSkipOverlayAnalysis( - codeql, - ["javascript"], - makeDiskUsage(50), - logger, - ); - - t.false(result); - t.true( - messages.some( - (m) => - m.type === "debug" && - typeof m.message === "string" && - m.message.includes("No overlay status found in Actions cache."), - ), - ); - }); -}); - -test("shouldSkipOverlayAnalysis returns true when cached status indicates failed build", async (t) => { - await withTmpDir(async (tmpDir) => { - process.env["RUNNER_TEMP"] = tmpDir; - const codeql = mockCodeQLVersion("2.20.0"); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - - const status = { - attemptedToBuildOverlayBaseDatabase: true, - builtOverlayBaseDatabase: false, - }; - - // Stub restoreCache to write the status file and return a key - sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => { - const statusFile = paths[0]; - await fs.promises.mkdir(path.dirname(statusFile), { recursive: true }); - await fs.promises.writeFile(statusFile, JSON.stringify(status)); - return "found-key"; +test.serial( + "shouldSkipOverlayAnalysis returns false when no cached status exists", + async (t) => { + await withTmpDir(async (tmpDir) => { + process.env["RUNNER_TEMP"] = tmpDir; + const codeql = mockCodeQLVersion("2.20.0"); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + + sinon.stub(actionsCache, "restoreCache").resolves(undefined); + + const result = await shouldSkipOverlayAnalysis( + codeql, + ["javascript"], + makeDiskUsage(50), + logger, + ); + + t.false(result); + t.true( + messages.some( + (m) => + m.type === "debug" && + typeof m.message === "string" && + m.message.includes("No overlay status found in Actions cache."), + ), + ); }); - - const result = await shouldSkipOverlayAnalysis( - codeql, - ["javascript"], - makeDiskUsage(50), - logger, - ); - - t.true(result); - }); -}); - -test("shouldSkipOverlayAnalysis returns false when cached status indicates successful build", async (t) => { - await withTmpDir(async (tmpDir) => { - process.env["RUNNER_TEMP"] = tmpDir; - const codeql = mockCodeQLVersion("2.20.0"); - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); - - const status = { - attemptedToBuildOverlayBaseDatabase: true, - builtOverlayBaseDatabase: true, - }; - - sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => { - const statusFile = paths[0]; - await fs.promises.mkdir(path.dirname(statusFile), { recursive: true }); - await fs.promises.writeFile(statusFile, JSON.stringify(status)); - return "found-key"; + }, +); + +test.serial( + "shouldSkipOverlayAnalysis returns true when cached status indicates failed build", + async (t) => { + await withTmpDir(async (tmpDir) => { + process.env["RUNNER_TEMP"] = tmpDir; + const codeql = mockCodeQLVersion("2.20.0"); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + + const status = { + attemptedToBuildOverlayBaseDatabase: true, + builtOverlayBaseDatabase: false, + }; + + // Stub restoreCache to write the status file and return a key + sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => { + const statusFile = paths[0]; + await fs.promises.mkdir(path.dirname(statusFile), { recursive: true }); + await fs.promises.writeFile(statusFile, JSON.stringify(status)); + return "found-key"; + }); + + const result = await shouldSkipOverlayAnalysis( + codeql, + ["javascript"], + makeDiskUsage(50), + logger, + ); + + t.true(result); }); - - const result = await shouldSkipOverlayAnalysis( - codeql, - ["javascript"], - makeDiskUsage(50), - logger, - ); - - t.false(result); - t.true( - messages.some( - (m) => - m.type === "debug" && - typeof m.message === "string" && - m.message.includes( - "Cached overlay status does not indicate a previous unsuccessful attempt", - ), - ), - ); - }); -}); + }, +); + +test.serial( + "shouldSkipOverlayAnalysis returns false when cached status indicates successful build", + async (t) => { + await withTmpDir(async (tmpDir) => { + process.env["RUNNER_TEMP"] = tmpDir; + const codeql = mockCodeQLVersion("2.20.0"); + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); + + const status = { + attemptedToBuildOverlayBaseDatabase: true, + builtOverlayBaseDatabase: true, + }; + + sinon.stub(actionsCache, "restoreCache").callsFake(async (paths) => { + const statusFile = paths[0]; + await fs.promises.mkdir(path.dirname(statusFile), { recursive: true }); + await fs.promises.writeFile(statusFile, JSON.stringify(status)); + return "found-key"; + }); + + const result = await shouldSkipOverlayAnalysis( + codeql, + ["javascript"], + makeDiskUsage(50), + logger, + ); + + t.false(result); + t.true( + messages.some( + (m) => + m.type === "debug" && + typeof m.message === "string" && + m.message.includes( + "Cached overlay status does not indicate a previous unsuccessful attempt", + ), + ), + ); + }); + }, +); diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index 91ace0196c..555352bd21 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -45,7 +45,7 @@ test.beforeEach(() => { initializeEnvironment("1.2.3"); }); -test("parse codeql bundle url version", (t) => { +test.serial("parse codeql bundle url version", (t) => { t.deepEqual( setupCodeql.getCodeQLURLVersion( "https://github.com/.../codeql-bundle-20200601/...", @@ -54,7 +54,7 @@ test("parse codeql bundle url version", (t) => { ); }); -test("convert to semver", (t) => { +test.serial("convert to semver", (t) => { const tests = { "20200601": "0.0.0-20200601", "20200601.0": "0.0.0-20200601.0", @@ -77,7 +77,7 @@ test("convert to semver", (t) => { } }); -test("getCodeQLActionRepository", (t) => { +test.serial("getCodeQLActionRepository", (t) => { const logger = getRunnerLogger(true); initializeEnvironment("1.2.3"); @@ -95,359 +95,383 @@ test("getCodeQLActionRepository", (t) => { t.deepEqual(repoEnv, "xxx/yyy"); }); -test("getCodeQLSource sets CLI version for a semver tagged bundle", async (t) => { - const features = createFeatures([]); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const tagName = "codeql-bundle-v1.2.3"; - mockBundleDownloadApi({ tagName }); - const source = await setupCodeql.getCodeQLSource( - `https://github.com/github/codeql-action/releases/download/${tagName}/codeql-bundle-linux64.tar.gz`, - SAMPLE_DEFAULT_CLI_VERSION, - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - false, - features, - getRunnerLogger(true), - ); +test.serial( + "getCodeQLSource sets CLI version for a semver tagged bundle", + async (t) => { + const features = createFeatures([]); - t.is(source.sourceType, "download"); - t.is(source["cliVersion"], "1.2.3"); - }); -}); + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const tagName = "codeql-bundle-v1.2.3"; + mockBundleDownloadApi({ tagName }); + const source = await setupCodeql.getCodeQLSource( + `https://github.com/github/codeql-action/releases/download/${tagName}/codeql-bundle-linux64.tar.gz`, + SAMPLE_DEFAULT_CLI_VERSION, + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + false, + features, + getRunnerLogger(true), + ); -test("getCodeQLSource correctly returns bundled CLI version when tools == linked", async (t) => { - const features = createFeatures([]); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const source = await setupCodeql.getCodeQLSource( - "linked", - SAMPLE_DEFAULT_CLI_VERSION, - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - false, - features, - getRunnerLogger(true), - ); + t.is(source.sourceType, "download"); + t.is(source["cliVersion"], "1.2.3"); + }); + }, +); - t.is(source.toolsVersion, LINKED_CLI_VERSION.cliVersion); - t.is(source.sourceType, "download"); - }); -}); +test.serial( + "getCodeQLSource correctly returns bundled CLI version when tools == linked", + async (t) => { + const features = createFeatures([]); -test("getCodeQLSource correctly returns bundled CLI version when tools == latest", async (t) => { - const loggedMessages: LoggedMessage[] = []; - const logger = getRecordingLogger(loggedMessages); - const features = createFeatures([]); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const source = await setupCodeql.getCodeQLSource( - "latest", - SAMPLE_DEFAULT_CLI_VERSION, - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - false, - features, - logger, - ); + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const source = await setupCodeql.getCodeQLSource( + "linked", + SAMPLE_DEFAULT_CLI_VERSION, + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + false, + features, + getRunnerLogger(true), + ); - // First, ensure that the CLI version is the linked version, so that backwards - // compatibility is maintained. - t.is(source.toolsVersion, LINKED_CLI_VERSION.cliVersion); - t.is(source.sourceType, "download"); - - // Afterwards, ensure that we see the deprecation message in the log. - const expected_message: string = - "`tools: latest` has been renamed to `tools: linked`, but the old name is still supported. No action is required."; - t.assert( - loggedMessages.some( - (msg) => - typeof msg.message === "string" && - msg.message.includes(expected_message), - ), - ); - }); -}); + t.is(source.toolsVersion, LINKED_CLI_VERSION.cliVersion); + t.is(source.sourceType, "download"); + }); + }, +); -test("setupCodeQLBundle logs the CodeQL CLI version being used when asked to use linked tools", async (t) => { - const loggedMessages: LoggedMessage[] = []; - const logger = getRecordingLogger(loggedMessages); - const features = createFeatures([]); - - // Stub the downloadCodeQL function to prevent downloading artefacts - // during testing from being called. - sinon.stub(setupCodeql, "downloadCodeQL").resolves({ - codeqlFolder: "codeql", - statusReport: { - combinedDurationMs: 500, - compressionMethod: "gzip", - downloadDurationMs: 200, - extractionDurationMs: 300, - streamExtraction: false, - toolsUrl: "toolsUrl", - }, - toolsVersion: LINKED_CLI_VERSION.cliVersion, - }); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const result = await setupCodeql.setupCodeQLBundle( - "linked", - SAMPLE_DOTCOM_API_DETAILS, - "tmp/codeql_action_test/", - GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - logger, - ); +test.serial( + "getCodeQLSource correctly returns bundled CLI version when tools == latest", + async (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + const features = createFeatures([]); - // Basic sanity check that the version we got back is indeed - // the linked (default) CLI version. - t.is(result.toolsVersion, LINKED_CLI_VERSION.cliVersion); - - // Ensure message logging CodeQL CLI version was present in user logs. - const expected_message: string = `Using CodeQL CLI version ${LINKED_CLI_VERSION.cliVersion}`; - t.assert( - loggedMessages.some( - (msg) => - typeof msg.message === "string" && - msg.message.includes(expected_message), - ), - ); - }); -}); + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const source = await setupCodeql.getCodeQLSource( + "latest", + SAMPLE_DEFAULT_CLI_VERSION, + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + false, + features, + logger, + ); -test("setupCodeQLBundle logs the CodeQL CLI version being used when asked to download a non-default bundle", async (t) => { - const loggedMessages: LoggedMessage[] = []; - const logger = getRecordingLogger(loggedMessages); - const features = createFeatures([]); - - const bundleUrl = - "https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.16.0/codeql-bundle-linux64.tar.gz"; - const expectedVersion = "2.16.0"; - - // Stub the downloadCodeQL function to prevent downloading artefacts - // during testing from being called. - sinon.stub(setupCodeql, "downloadCodeQL").resolves({ - codeqlFolder: "codeql", - statusReport: { - combinedDurationMs: 500, - compressionMethod: "gzip", - downloadDurationMs: 200, - extractionDurationMs: 300, - streamExtraction: false, - toolsUrl: bundleUrl, - }, - toolsVersion: expectedVersion, - }); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const result = await setupCodeql.setupCodeQLBundle( - bundleUrl, - SAMPLE_DOTCOM_API_DETAILS, - "tmp/codeql_action_test/", - GitHubVariant.DOTCOM, - SAMPLE_DEFAULT_CLI_VERSION, - features, - logger, - ); + // First, ensure that the CLI version is the linked version, so that backwards + // compatibility is maintained. + t.is(source.toolsVersion, LINKED_CLI_VERSION.cliVersion); + t.is(source.sourceType, "download"); - // Basic sanity check that the version we got back is indeed the version that the - // bundle contains.. - t.is(result.toolsVersion, expectedVersion); - - // Ensure message logging CodeQL CLI version was present in user logs. - const expected_message: string = `Using CodeQL CLI version 2.16.0 sourced from ${bundleUrl} .`; - t.assert( - loggedMessages.some( - (msg) => - typeof msg.message === "string" && - msg.message.includes(expected_message), - ), - ); - }); -}); + // Afterwards, ensure that we see the deprecation message in the log. + const expected_message: string = + "`tools: latest` has been renamed to `tools: linked`, but the old name is still supported. No action is required."; + t.assert( + loggedMessages.some( + (msg) => + typeof msg.message === "string" && + msg.message.includes(expected_message), + ), + ); + }); + }, +); -test("getCodeQLSource correctly returns nightly CLI version when tools == nightly", async (t) => { - const loggedMessages: LoggedMessage[] = []; - const logger = getRecordingLogger(loggedMessages); - const features = createFeatures([]); - - const expectedDate = "30260213"; - const expectedTag = `codeql-bundle-${expectedDate}`; - - // Ensure that we consistently select "zstd" for the test. - sinon.stub(process, "platform").value("linux"); - sinon.stub(tar, "isZstdAvailable").resolves({ - available: true, - foundZstdBinary: true, - }); - - const client = github.getOctokit("123"); - const listReleases = sinon.stub(client.rest.repos, "listReleases"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - listReleases.resolves({ - data: [{ tag_name: expectedTag }], - } as any); - sinon.stub(api, "getApiClient").value(() => client); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); - const source = await setupCodeql.getCodeQLSource( - "nightly", - SAMPLE_DEFAULT_CLI_VERSION, - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - false, - features, - logger, - ); +test.serial( + "setupCodeQLBundle logs the CodeQL CLI version being used when asked to use linked tools", + async (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + const features = createFeatures([]); + + // Stub the downloadCodeQL function to prevent downloading artefacts + // during testing from being called. + sinon.stub(setupCodeql, "downloadCodeQL").resolves({ + codeqlFolder: "codeql", + statusReport: { + combinedDurationMs: 500, + compressionMethod: "gzip", + downloadDurationMs: 200, + extractionDurationMs: 300, + streamExtraction: false, + toolsUrl: "toolsUrl", + }, + toolsVersion: LINKED_CLI_VERSION.cliVersion, + }); - // Check that the `CodeQLToolsSource` object matches our expectations. - const expectedVersion = `0.0.0-${expectedDate}`; - const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`; - t.deepEqual(source, { - bundleVersion: expectedDate, - cliVersion: undefined, - codeqlURL: expectedURL, - compressionMethod: "zstd", - sourceType: "download", - toolsVersion: expectedVersion, - } satisfies setupCodeql.CodeQLToolsSource); - - // Afterwards, ensure that we see the expected messages in the log. - checkExpectedLogMessages(t, loggedMessages, [ - "Using the latest CodeQL CLI nightly, as requested by 'tools: nightly'.", - `Bundle version ${expectedDate} is not in SemVer format. Will treat it as pre-release ${expectedVersion}.`, - `Attempting to obtain CodeQL tools. CLI version: unknown, bundle tag name: ${expectedTag}`, - `Using CodeQL CLI sourced from ${expectedURL}`, - ]); - }); -}); + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const result = await setupCodeql.setupCodeQLBundle( + "linked", + SAMPLE_DOTCOM_API_DETAILS, + "tmp/codeql_action_test/", + GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + logger, + ); -test("getCodeQLSource correctly returns nightly CLI version when forced by FF", async (t) => { - const loggedMessages: LoggedMessage[] = []; - const logger = getRecordingLogger(loggedMessages); - const features = createFeatures([Feature.ForceNightly]); - - const expectedDate = "30260213"; - const expectedTag = `codeql-bundle-${expectedDate}`; - - // Ensure that we consistently select "zstd" for the test. - sinon.stub(process, "platform").value("linux"); - sinon.stub(tar, "isZstdAvailable").resolves({ - available: true, - foundZstdBinary: true, - }); - - const client = github.getOctokit("123"); - const listReleases = sinon.stub(client.rest.repos, "listReleases"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - listReleases.resolves({ - data: [{ tag_name: expectedTag }], - } as any); - sinon.stub(api, "getApiClient").value(() => client); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" }); - - const source = await setupCodeql.getCodeQLSource( - undefined, - SAMPLE_DEFAULT_CLI_VERSION, - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - false, - features, - logger, - ); + // Basic sanity check that the version we got back is indeed + // the linked (default) CLI version. + t.is(result.toolsVersion, LINKED_CLI_VERSION.cliVersion); + + // Ensure message logging CodeQL CLI version was present in user logs. + const expected_message: string = `Using CodeQL CLI version ${LINKED_CLI_VERSION.cliVersion}`; + t.assert( + loggedMessages.some( + (msg) => + typeof msg.message === "string" && + msg.message.includes(expected_message), + ), + ); + }); + }, +); - // Check that the `CodeQLToolsSource` object matches our expectations. - const expectedVersion = `0.0.0-${expectedDate}`; - const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`; - t.deepEqual(source, { - bundleVersion: expectedDate, - cliVersion: undefined, - codeqlURL: expectedURL, - compressionMethod: "zstd", - sourceType: "download", +test.serial( + "setupCodeQLBundle logs the CodeQL CLI version being used when asked to download a non-default bundle", + async (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + const features = createFeatures([]); + + const bundleUrl = + "https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.16.0/codeql-bundle-linux64.tar.gz"; + const expectedVersion = "2.16.0"; + + // Stub the downloadCodeQL function to prevent downloading artefacts + // during testing from being called. + sinon.stub(setupCodeql, "downloadCodeQL").resolves({ + codeqlFolder: "codeql", + statusReport: { + combinedDurationMs: 500, + compressionMethod: "gzip", + downloadDurationMs: 200, + extractionDurationMs: 300, + streamExtraction: false, + toolsUrl: bundleUrl, + }, toolsVersion: expectedVersion, - } satisfies setupCodeql.CodeQLToolsSource); - - // Afterwards, ensure that we see the expected messages in the log. - checkExpectedLogMessages(t, loggedMessages, [ - `Using the latest CodeQL CLI nightly, as forced by the ${Feature.ForceNightly} feature flag.`, - `Bundle version ${expectedDate} is not in SemVer format. Will treat it as pre-release ${expectedVersion}.`, - `Attempting to obtain CodeQL tools. CLI version: unknown, bundle tag name: ${expectedTag}`, - `Using CodeQL CLI sourced from ${expectedURL}`, - ]); - }); -}); + }); -test("getCodeQLSource correctly returns latest version from toolcache when tools == toolcache", async (t) => { - const loggedMessages: LoggedMessage[] = []; - const logger = getRecordingLogger(loggedMessages); - const features = createFeatures([Feature.AllowToolcacheInput]); - - const latestToolcacheVersion = "3.2.1"; - const latestVersionPath = "/path/to/latest"; - const testVersions = ["2.3.1", latestToolcacheVersion, "1.2.3"]; - const findAllVersionsStub = sinon - .stub(toolcache, "findAllVersions") - .returns(testVersions); - const findStub = sinon.stub(toolcache, "find"); - findStub - .withArgs("CodeQL", latestToolcacheVersion) - .returns(latestVersionPath); - - await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" }); - - const source = await setupCodeql.getCodeQLSource( - "toolcache", - SAMPLE_DEFAULT_CLI_VERSION, - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - false, - features, - logger, - ); + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const result = await setupCodeql.setupCodeQLBundle( + bundleUrl, + SAMPLE_DOTCOM_API_DETAILS, + "tmp/codeql_action_test/", + GitHubVariant.DOTCOM, + SAMPLE_DEFAULT_CLI_VERSION, + features, + logger, + ); - // Check that the toolcache functions were called with the expected arguments - t.assert( - findAllVersionsStub.calledOnceWith("CodeQL"), - `toolcache.findAllVersions("CodeQL") wasn't called`, - ); - t.assert( - findStub.calledOnceWith("CodeQL", latestToolcacheVersion), - `toolcache.find("CodeQL", ${latestToolcacheVersion}) wasn't called`, - ); + // Basic sanity check that the version we got back is indeed the version that the + // bundle contains.. + t.is(result.toolsVersion, expectedVersion); - // Check that `sourceType` and `toolsVersion` match expectations. - t.is(source.sourceType, "toolcache"); - t.is(source.toolsVersion, latestToolcacheVersion); - - // Check that key messages we would expect to find in the log are present. - const expectedMessages: string[] = [ - `Attempting to use the latest CodeQL CLI version in the toolcache, as requested by 'tools: toolcache'.`, - `CLI version ${latestToolcacheVersion} is the latest version in the toolcache.`, - `Using CodeQL CLI version ${latestToolcacheVersion} from toolcache at ${latestVersionPath}`, - ]; - for (const expectedMessage of expectedMessages) { + // Ensure message logging CodeQL CLI version was present in user logs. + const expected_message: string = `Using CodeQL CLI version 2.16.0 sourced from ${bundleUrl} .`; t.assert( loggedMessages.some( (msg) => typeof msg.message === "string" && - msg.message.includes(expectedMessage), + msg.message.includes(expected_message), ), - `Expected '${expectedMessage}' in the logger output, but didn't find it in:\n ${loggedMessages.map((m) => ` - '${m.message}'`).join("\n")}`, ); - } - }); -}); + }); + }, +); + +test.serial( + "getCodeQLSource correctly returns nightly CLI version when tools == nightly", + async (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + const features = createFeatures([]); + + const expectedDate = "30260213"; + const expectedTag = `codeql-bundle-${expectedDate}`; + + // Ensure that we consistently select "zstd" for the test. + sinon.stub(process, "platform").value("linux"); + sinon.stub(tar, "isZstdAvailable").resolves({ + available: true, + foundZstdBinary: true, + }); + + const client = github.getOctokit("123"); + const listReleases = sinon.stub(client.rest.repos, "listReleases"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + listReleases.resolves({ + data: [{ tag_name: expectedTag }], + } as any); + sinon.stub(api, "getApiClient").value(() => client); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const source = await setupCodeql.getCodeQLSource( + "nightly", + SAMPLE_DEFAULT_CLI_VERSION, + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + false, + features, + logger, + ); + + // Check that the `CodeQLToolsSource` object matches our expectations. + const expectedVersion = `0.0.0-${expectedDate}`; + const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`; + t.deepEqual(source, { + bundleVersion: expectedDate, + cliVersion: undefined, + codeqlURL: expectedURL, + compressionMethod: "zstd", + sourceType: "download", + toolsVersion: expectedVersion, + } satisfies setupCodeql.CodeQLToolsSource); + + // Afterwards, ensure that we see the expected messages in the log. + checkExpectedLogMessages(t, loggedMessages, [ + "Using the latest CodeQL CLI nightly, as requested by 'tools: nightly'.", + `Bundle version ${expectedDate} is not in SemVer format. Will treat it as pre-release ${expectedVersion}.`, + `Attempting to obtain CodeQL tools. CLI version: unknown, bundle tag name: ${expectedTag}`, + `Using CodeQL CLI sourced from ${expectedURL}`, + ]); + }); + }, +); + +test.serial( + "getCodeQLSource correctly returns nightly CLI version when forced by FF", + async (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + const features = createFeatures([Feature.ForceNightly]); + + const expectedDate = "30260213"; + const expectedTag = `codeql-bundle-${expectedDate}`; + + // Ensure that we consistently select "zstd" for the test. + sinon.stub(process, "platform").value("linux"); + sinon.stub(tar, "isZstdAvailable").resolves({ + available: true, + foundZstdBinary: true, + }); + + const client = github.getOctokit("123"); + const listReleases = sinon.stub(client.rest.repos, "listReleases"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + listReleases.resolves({ + data: [{ tag_name: expectedTag }], + } as any); + sinon.stub(api, "getApiClient").value(() => client); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" }); + + const source = await setupCodeql.getCodeQLSource( + undefined, + SAMPLE_DEFAULT_CLI_VERSION, + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + false, + features, + logger, + ); + + // Check that the `CodeQLToolsSource` object matches our expectations. + const expectedVersion = `0.0.0-${expectedDate}`; + const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`; + t.deepEqual(source, { + bundleVersion: expectedDate, + cliVersion: undefined, + codeqlURL: expectedURL, + compressionMethod: "zstd", + sourceType: "download", + toolsVersion: expectedVersion, + } satisfies setupCodeql.CodeQLToolsSource); + + // Afterwards, ensure that we see the expected messages in the log. + checkExpectedLogMessages(t, loggedMessages, [ + `Using the latest CodeQL CLI nightly, as forced by the ${Feature.ForceNightly} feature flag.`, + `Bundle version ${expectedDate} is not in SemVer format. Will treat it as pre-release ${expectedVersion}.`, + `Attempting to obtain CodeQL tools. CLI version: unknown, bundle tag name: ${expectedTag}`, + `Using CodeQL CLI sourced from ${expectedURL}`, + ]); + }); + }, +); + +test.serial( + "getCodeQLSource correctly returns latest version from toolcache when tools == toolcache", + async (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + const features = createFeatures([Feature.AllowToolcacheInput]); + + const latestToolcacheVersion = "3.2.1"; + const latestVersionPath = "/path/to/latest"; + const testVersions = ["2.3.1", latestToolcacheVersion, "1.2.3"]; + const findAllVersionsStub = sinon + .stub(toolcache, "findAllVersions") + .returns(testVersions); + const findStub = sinon.stub(toolcache, "find"); + findStub + .withArgs("CodeQL", latestToolcacheVersion) + .returns(latestVersionPath); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" }); + + const source = await setupCodeql.getCodeQLSource( + "toolcache", + SAMPLE_DEFAULT_CLI_VERSION, + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + false, + features, + logger, + ); + + // Check that the toolcache functions were called with the expected arguments + t.assert( + findAllVersionsStub.calledOnceWith("CodeQL"), + `toolcache.findAllVersions("CodeQL") wasn't called`, + ); + t.assert( + findStub.calledOnceWith("CodeQL", latestToolcacheVersion), + `toolcache.find("CodeQL", ${latestToolcacheVersion}) wasn't called`, + ); + + // Check that `sourceType` and `toolsVersion` match expectations. + t.is(source.sourceType, "toolcache"); + t.is(source.toolsVersion, latestToolcacheVersion); + + // Check that key messages we would expect to find in the log are present. + const expectedMessages: string[] = [ + `Attempting to use the latest CodeQL CLI version in the toolcache, as requested by 'tools: toolcache'.`, + `CLI version ${latestToolcacheVersion} is the latest version in the toolcache.`, + `Using CodeQL CLI version ${latestToolcacheVersion} from toolcache at ${latestVersionPath}`, + ]; + for (const expectedMessage of expectedMessages) { + t.assert( + loggedMessages.some( + (msg) => + typeof msg.message === "string" && + msg.message.includes(expectedMessage), + ), + `Expected '${expectedMessage}' in the logger output, but didn't find it in:\n ${loggedMessages.map((m) => ` - '${m.message}'`).join("\n")}`, + ); + } + }); + }, +); const toolcacheInputFallbackMacro = test.macro({ exec: async ( @@ -509,7 +533,7 @@ const toolcacheInputFallbackMacro = test.macro({ `getCodeQLSource falls back to downloading the CLI if ${providedTitle}`, }); -test( +test.serial( "the toolcache doesn't have a CodeQL CLI when tools == toolcache", toolcacheInputFallbackMacro, [Feature.AllowToolcacheInput], @@ -521,7 +545,7 @@ test( ], ); -test( +test.serial( "the workflow trigger is not `dynamic`", toolcacheInputFallbackMacro, [Feature.AllowToolcacheInput], @@ -532,7 +556,7 @@ test( ], ); -test( +test.serial( "the feature flag is not enabled", toolcacheInputFallbackMacro, [], @@ -541,24 +565,36 @@ test( [`Ignoring 'tools: toolcache' because the feature is not enabled.`], ); -test('tryGetTagNameFromUrl extracts the right tag name for a repo name containing "codeql-bundle"', (t) => { - t.is( - setupCodeql.tryGetTagNameFromUrl( - "https://github.com/org/codeql-bundle-testing/releases/download/codeql-bundle-v2.19.0/codeql-bundle-linux64.tar.zst", - getRunnerLogger(true), - ), - "codeql-bundle-v2.19.0", - ); -}); +test.serial( + 'tryGetTagNameFromUrl extracts the right tag name for a repo name containing "codeql-bundle"', + (t) => { + t.is( + setupCodeql.tryGetTagNameFromUrl( + "https://github.com/org/codeql-bundle-testing/releases/download/codeql-bundle-v2.19.0/codeql-bundle-linux64.tar.zst", + getRunnerLogger(true), + ), + "codeql-bundle-v2.19.0", + ); + }, +); -test("getLatestToolcacheVersion returns undefined if there are no CodeQL CLIs in the toolcache", (t) => { - sinon.stub(toolcache, "findAllVersions").returns([]); - t.is(setupCodeql.getLatestToolcacheVersion(getRunnerLogger(true)), undefined); -}); +test.serial( + "getLatestToolcacheVersion returns undefined if there are no CodeQL CLIs in the toolcache", + (t) => { + sinon.stub(toolcache, "findAllVersions").returns([]); + t.is( + setupCodeql.getLatestToolcacheVersion(getRunnerLogger(true)), + undefined, + ); + }, +); -test("getLatestToolcacheVersion returns latest version in the toolcache", (t) => { - const testVersions = ["2.3.1", "3.2.1", "1.2.3"]; - sinon.stub(toolcache, "findAllVersions").returns(testVersions); +test.serial( + "getLatestToolcacheVersion returns latest version in the toolcache", + (t) => { + const testVersions = ["2.3.1", "3.2.1", "1.2.3"]; + sinon.stub(toolcache, "findAllVersions").returns(testVersions); - t.is(setupCodeql.getLatestToolcacheVersion(getRunnerLogger(true)), "3.2.1"); -}); + t.is(setupCodeql.getLatestToolcacheVersion(getRunnerLogger(true)), "3.2.1"); + }, +); diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index 52456fe427..a4dd8d589a 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -87,14 +87,14 @@ const sendFailedStatusReportTest = test.macro({ title: (providedTitle = "") => `sendFailedStatusReport - ${providedTitle}`, }); -test( +test.serial( "reports generic error message for non-StartProxyError error", sendFailedStatusReportTest, new Error("Something went wrong today"), "Error from start-proxy Action omitted (Error).", ); -test( +test.serial( "reports generic error message for non-StartProxyError error with safe error message", sendFailedStatusReportTest, new Error( @@ -105,7 +105,7 @@ test( "Error from start-proxy Action omitted (Error).", ); -test( +test.serial( "reports generic error message for ConfigurationError error", sendFailedStatusReportTest, new ConfigurationError("Something went wrong today"), @@ -124,110 +124,125 @@ const mixedCredentials = [ { type: "git_source", host: "github.com/github", token: "mno" }, ]; -test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => { - const registryCredentials = Buffer.from( - JSON.stringify([ - { type: "npm_registry", host: "npm.pkg.github.com", token: "abc" }, - ]), - ).toString("base64"); - const registrySecrets = JSON.stringify([ - { type: "npm_registry", host: "registry.npmjs.org", token: "def" }, - ]); - - const credentials = startProxyExports.getCredentials( - getRunnerLogger(true), - registrySecrets, - registryCredentials, - undefined, - ); - t.is(credentials.length, 1); - t.is(credentials[0].host, "npm.pkg.github.com"); -}); - -test("getCredentials throws an error when configurations are not an array", async (t) => { - const registryCredentials = Buffer.from( - JSON.stringify({ type: "npm_registry", token: "abc" }), - ).toString("base64"); +test.serial( + "getCredentials prefers registriesCredentials over registrySecrets", + async (t) => { + const registryCredentials = Buffer.from( + JSON.stringify([ + { type: "npm_registry", host: "npm.pkg.github.com", token: "abc" }, + ]), + ).toString("base64"); + const registrySecrets = JSON.stringify([ + { type: "npm_registry", host: "registry.npmjs.org", token: "def" }, + ]); - t.throws( - () => - startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - registryCredentials, - undefined, - ), - { - message: - "Expected credentials data to be an array of configurations, but it is not.", - }, - ); -}); + const credentials = startProxyExports.getCredentials( + getRunnerLogger(true), + registrySecrets, + registryCredentials, + undefined, + ); + t.is(credentials.length, 1); + t.is(credentials[0].host, "npm.pkg.github.com"); + }, +); -test("getCredentials throws error when credential is not an object", async (t) => { - const testCredentials = [["foo"], [null]].map(toEncodedJSON); +test.serial( + "getCredentials throws an error when configurations are not an array", + async (t) => { + const registryCredentials = Buffer.from( + JSON.stringify({ type: "npm_registry", token: "abc" }), + ).toString("base64"); - for (const testCredential of testCredentials) { t.throws( () => startProxyExports.getCredentials( getRunnerLogger(true), undefined, - testCredential, + registryCredentials, undefined, ), { - message: "Invalid credentials - must be an object", + message: + "Expected credentials data to be an array of configurations, but it is not.", }, ); - } -}); + }, +); -test("getCredentials throws error when credential is missing type", async (t) => { - const testCredentials = [[{ token: "abc", url: "https://localhost" }]].map( - toEncodedJSON, - ); +test.serial( + "getCredentials throws error when credential is not an object", + async (t) => { + const testCredentials = [["foo"], [null]].map(toEncodedJSON); + + for (const testCredential of testCredentials) { + t.throws( + () => + startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + testCredential, + undefined, + ), + { + message: "Invalid credentials - must be an object", + }, + ); + } + }, +); - for (const testCredential of testCredentials) { - t.throws( - () => - startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - testCredential, - undefined, - ), - { - message: "Invalid credentials - must have a type", - }, +test.serial( + "getCredentials throws error when credential is missing type", + async (t) => { + const testCredentials = [[{ token: "abc", url: "https://localhost" }]].map( + toEncodedJSON, ); - } -}); -test("getCredentials throws error when credential missing host and url", async (t) => { - const testCredentials = [ - [{ type: "npm_registry", token: "abc" }], - [{ type: "npm_registry", token: "abc", host: null }], - [{ type: "npm_registry", token: "abc", url: null }], - ].map(toEncodedJSON); + for (const testCredential of testCredentials) { + t.throws( + () => + startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + testCredential, + undefined, + ), + { + message: "Invalid credentials - must have a type", + }, + ); + } + }, +); - for (const testCredential of testCredentials) { - t.throws( - () => - startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - testCredential, - undefined, - ), - { - message: "Invalid credentials - must specify host or url", - }, - ); - } -}); +test.serial( + "getCredentials throws error when credential missing host and url", + async (t) => { + const testCredentials = [ + [{ type: "npm_registry", token: "abc" }], + [{ type: "npm_registry", token: "abc", host: null }], + [{ type: "npm_registry", token: "abc", url: null }], + ].map(toEncodedJSON); + + for (const testCredential of testCredentials) { + t.throws( + () => + startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + testCredential, + undefined, + ), + { + message: "Invalid credentials - must specify host or url", + }, + ); + } + }, +); -test("getCredentials filters by language when specified", async (t) => { +test.serial("getCredentials filters by language when specified", async (t) => { const credentials = startProxyExports.getCredentials( getRunnerLogger(true), undefined, @@ -238,123 +253,145 @@ test("getCredentials filters by language when specified", async (t) => { t.is(credentials[0].type, "maven_repository"); }); -test("getCredentials returns all for a language when specified", async (t) => { - const credentials = startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - toEncodedJSON(mixedCredentials), - KnownLanguage.go, - ); - t.is(credentials.length, 2); - - const credentialsTypes = credentials.map((c) => c.type); - t.assert(credentialsTypes.includes("goproxy_server")); - t.assert(credentialsTypes.includes("git_source")); -}); +test.serial( + "getCredentials returns all for a language when specified", + async (t) => { + const credentials = startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + toEncodedJSON(mixedCredentials), + KnownLanguage.go, + ); + t.is(credentials.length, 2); -test("getCredentials returns all credentials when no language specified", async (t) => { - const credentialsInput = toEncodedJSON(mixedCredentials); + const credentialsTypes = credentials.map((c) => c.type); + t.assert(credentialsTypes.includes("goproxy_server")); + t.assert(credentialsTypes.includes("git_source")); + }, +); - const credentials = startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - credentialsInput, - undefined, - ); - t.is(credentials.length, mixedCredentials.length); -}); +test.serial( + "getCredentials returns all credentials when no language specified", + async (t) => { + const credentialsInput = toEncodedJSON(mixedCredentials); -test("getCredentials throws an error when non-printable characters are used", async (t) => { - const invalidCredentials = [ - { type: "nuget_feed", host: "1nuget.pkg.github.com", token: "abc\u0000" }, // Non-printable character in token - { type: "nuget_feed", host: "2nuget.pkg.github.com\u0001" }, // Non-printable character in host - { - type: "nuget_feed", - host: "3nuget.pkg.github.com", - password: "ghi\u0002", - }, // Non-printable character in password - { type: "nuget_feed", host: "4nuget.pkg.github.com", password: "ghi\x00" }, // Non-printable character in password - ]; + const credentials = startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + credentialsInput, + undefined, + ); + t.is(credentials.length, mixedCredentials.length); + }, +); - for (const invalidCredential of invalidCredentials) { - const credentialsInput = Buffer.from( - JSON.stringify([invalidCredential]), - ).toString("base64"); +test.serial( + "getCredentials throws an error when non-printable characters are used", + async (t) => { + const invalidCredentials = [ + { type: "nuget_feed", host: "1nuget.pkg.github.com", token: "abc\u0000" }, // Non-printable character in token + { type: "nuget_feed", host: "2nuget.pkg.github.com\u0001" }, // Non-printable character in host + { + type: "nuget_feed", + host: "3nuget.pkg.github.com", + password: "ghi\u0002", + }, // Non-printable character in password + { + type: "nuget_feed", + host: "4nuget.pkg.github.com", + password: "ghi\x00", + }, // Non-printable character in password + ]; + + for (const invalidCredential of invalidCredentials) { + const credentialsInput = Buffer.from( + JSON.stringify([invalidCredential]), + ).toString("base64"); + + t.throws( + () => + startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + credentialsInput, + undefined, + ), + { + message: + "Invalid credentials - fields must contain only printable characters", + }, + ); + } + }, +); - t.throws( - () => - startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - credentialsInput, - undefined, - ), +test.serial( + "getCredentials logs a warning when a PAT is used without a username", + async (t) => { + const loggedMessages = []; + const logger = getRecordingLogger(loggedMessages); + const likelyWrongCredentials = toEncodedJSON([ { - message: - "Invalid credentials - fields must contain only printable characters", + type: "git_server", + host: "https://github.com/", + password: `ghp_${makeTestToken()}`, }, - ); - } -}); + ]); -test("getCredentials logs a warning when a PAT is used without a username", async (t) => { - const loggedMessages = []; - const logger = getRecordingLogger(loggedMessages); - const likelyWrongCredentials = toEncodedJSON([ - { - type: "git_server", - host: "https://github.com/", - password: `ghp_${makeTestToken()}`, - }, - ]); - - const results = startProxyExports.getCredentials( - logger, - undefined, - likelyWrongCredentials, - undefined, - ); + const results = startProxyExports.getCredentials( + logger, + undefined, + likelyWrongCredentials, + undefined, + ); - // The configuration should be accepted, despite the likely problem. - t.assert(results); - t.is(results.length, 1); - t.is(results[0].type, "git_server"); - t.is(results[0].host, "https://github.com/"); - t.assert(results[0].password?.startsWith("ghp_")); - - // A warning should have been logged. - checkExpectedLogMessages(t, loggedMessages, [ - "using a GitHub Personal Access Token (PAT), but no username was provided", - ]); -}); + // The configuration should be accepted, despite the likely problem. + t.assert(results); + t.is(results.length, 1); + t.is(results[0].type, "git_server"); + t.is(results[0].host, "https://github.com/"); + t.assert(results[0].password?.startsWith("ghp_")); + + // A warning should have been logged. + checkExpectedLogMessages(t, loggedMessages, [ + "using a GitHub Personal Access Token (PAT), but no username was provided", + ]); + }, +); -test("getCredentials returns all credentials for Actions when using LANGUAGE_TO_REGISTRY_TYPE", async (t) => { - const credentialsInput = toEncodedJSON(mixedCredentials); +test.serial( + "getCredentials returns all credentials for Actions when using LANGUAGE_TO_REGISTRY_TYPE", + async (t) => { + const credentialsInput = toEncodedJSON(mixedCredentials); - const credentials = startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - credentialsInput, - KnownLanguage.actions, - false, - ); - t.is(credentials.length, mixedCredentials.length); -}); + const credentials = startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + credentialsInput, + KnownLanguage.actions, + false, + ); + t.is(credentials.length, mixedCredentials.length); + }, +); -test("getCredentials returns no credentials for Actions when using NEW_LANGUAGE_TO_REGISTRY_TYPE", async (t) => { - const credentialsInput = toEncodedJSON(mixedCredentials); +test.serial( + "getCredentials returns no credentials for Actions when using NEW_LANGUAGE_TO_REGISTRY_TYPE", + async (t) => { + const credentialsInput = toEncodedJSON(mixedCredentials); - const credentials = startProxyExports.getCredentials( - getRunnerLogger(true), - undefined, - credentialsInput, - KnownLanguage.actions, - true, - ); - t.deepEqual(credentials, []); -}); + const credentials = startProxyExports.getCredentials( + getRunnerLogger(true), + undefined, + credentialsInput, + KnownLanguage.actions, + true, + ); + t.deepEqual(credentials, []); + }, +); -test("parseLanguage", async (t) => { +test.serial("parseLanguage", async (t) => { // Exact matches t.deepEqual(parseLanguage("csharp"), KnownLanguage.csharp); t.deepEqual(parseLanguage("cpp"), KnownLanguage.cpp); @@ -417,34 +454,14 @@ function mockOfflineFeatures(tempDir: string, logger: Logger) { return setUpFeatureFlagTests(tempDir, logger, gitHubVersion); } -test("getDownloadUrl returns fallback when `getReleaseByVersion` rejects", async (t) => { - const logger = new RecordingLogger(); - mockGetReleaseByTag(); - - await withTmpDir(async (tempDir) => { - const features = mockOfflineFeatures(tempDir, logger); - const info = await startProxyExports.getDownloadUrl( - getRunnerLogger(true), - features, - ); - - t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION); - t.is( - info.url, - startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()), - ); - }); -}); - -test("getDownloadUrl returns fallback when there's no matching release asset", async (t) => { - const logger = new RecordingLogger(); - const testAssets = [[], [{ name: "foo" }]]; - - await withTmpDir(async (tempDir) => { - const features = mockOfflineFeatures(tempDir, logger); +test.serial( + "getDownloadUrl returns fallback when `getReleaseByVersion` rejects", + async (t) => { + const logger = new RecordingLogger(); + mockGetReleaseByTag(); - for (const assets of testAssets) { - const stub = mockGetReleaseByTag(assets); + await withTmpDir(async (tempDir) => { + const features = mockOfflineFeatures(tempDir, logger); const info = await startProxyExports.getDownloadUrl( getRunnerLogger(true), features, @@ -455,13 +472,39 @@ test("getDownloadUrl returns fallback when there's no matching release asset", a info.url, startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()), ); + }); + }, +); - stub.restore(); - } - }); -}); +test.serial( + "getDownloadUrl returns fallback when there's no matching release asset", + async (t) => { + const logger = new RecordingLogger(); + const testAssets = [[], [{ name: "foo" }]]; -test("getDownloadUrl returns matching release asset", async (t) => { + await withTmpDir(async (tempDir) => { + const features = mockOfflineFeatures(tempDir, logger); + + for (const assets of testAssets) { + const stub = mockGetReleaseByTag(assets); + const info = await startProxyExports.getDownloadUrl( + getRunnerLogger(true), + features, + ); + + t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION); + t.is( + info.url, + startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()), + ); + + stub.restore(); + } + }); + }, +); + +test.serial("getDownloadUrl returns matching release asset", async (t) => { const logger = new RecordingLogger(); const assets = [ { name: "foo", url: "other-url" }, @@ -481,7 +524,7 @@ test("getDownloadUrl returns matching release asset", async (t) => { }); }); -test("credentialToStr - hides passwords", (t) => { +test.serial("credentialToStr - hides passwords", (t) => { const secret = "password123"; const credential = { type: "maven_credential", @@ -498,7 +541,7 @@ test("credentialToStr - hides passwords", (t) => { ); }); -test("credentialToStr - hides tokens", (t) => { +test.serial("credentialToStr - hides tokens", (t) => { const secret = "password123"; const credential = { type: "maven_credential", @@ -515,29 +558,35 @@ test("credentialToStr - hides tokens", (t) => { ); }); -test("getSafeErrorMessage - returns actual message for `StartProxyError`", (t) => { - const error = new startProxyExports.StartProxyError( - startProxyExports.StartProxyErrorType.DownloadFailed, - ); - t.is( - startProxyExports.getSafeErrorMessage(error), - startProxyExports.getStartProxyErrorMessage(error.errorType), - ); -}); - -test("getSafeErrorMessage - does not return message for arbitrary errors", (t) => { - const error = new Error( - startProxyExports.getStartProxyErrorMessage( +test.serial( + "getSafeErrorMessage - returns actual message for `StartProxyError`", + (t) => { + const error = new startProxyExports.StartProxyError( startProxyExports.StartProxyErrorType.DownloadFailed, - ), - ); + ); + t.is( + startProxyExports.getSafeErrorMessage(error), + startProxyExports.getStartProxyErrorMessage(error.errorType), + ); + }, +); - const message = startProxyExports.getSafeErrorMessage(error); +test.serial( + "getSafeErrorMessage - does not return message for arbitrary errors", + (t) => { + const error = new Error( + startProxyExports.getStartProxyErrorMessage( + startProxyExports.StartProxyErrorType.DownloadFailed, + ), + ); - t.not(message, error.message); - t.assert(message.startsWith("Error from start-proxy Action omitted")); - t.assert(message.includes(error.name)); -}); + const message = startProxyExports.getSafeErrorMessage(error); + + t.not(message, error.message); + t.assert(message.startsWith("Error from start-proxy Action omitted")); + t.assert(message.includes(error.name)); + }, +); const wrapFailureTest = test.macro({ exec: async ( @@ -556,7 +605,7 @@ const wrapFailureTest = test.macro({ title: (providedTitle) => `${providedTitle} - wraps errors on failure`, }); -test("downloadProxy - returns file path on success", async (t) => { +test.serial("downloadProxy - returns file path on success", async (t) => { await withRecordingLoggerAsync(async (logger) => { const testPath = "/some/path"; sinon.stub(toolcache, "downloadTool").resolves(testPath); @@ -570,7 +619,7 @@ test("downloadProxy - returns file path on success", async (t) => { }); }); -test( +test.serial( "downloadProxy", wrapFailureTest, () => { @@ -581,7 +630,7 @@ test( }, ); -test("extractProxy - returns file path on success", async (t) => { +test.serial("extractProxy - returns file path on success", async (t) => { await withRecordingLoggerAsync(async (logger) => { const testPath = "/some/path"; sinon.stub(toolcache, "extractTar").resolves(testPath); @@ -591,7 +640,7 @@ test("extractProxy - returns file path on success", async (t) => { }); }); -test( +test.serial( "extractProxy", wrapFailureTest, () => { @@ -602,7 +651,7 @@ test( }, ); -test("cacheProxy - returns file path on success", async (t) => { +test.serial("cacheProxy - returns file path on success", async (t) => { await withRecordingLoggerAsync(async (logger) => { const testPath = "/some/path"; sinon.stub(toolcache, "cacheDir").resolves(testPath); @@ -617,7 +666,7 @@ test("cacheProxy - returns file path on success", async (t) => { }); }); -test( +test.serial( "cacheProxy", wrapFailureTest, () => { @@ -628,100 +677,37 @@ test( }, ); -test("getProxyBinaryPath - returns path from tool cache if available", async (t) => { - const logger = new RecordingLogger(); - mockGetReleaseByTag(); - - await withTmpDir(async (tempDir) => { - const toolcachePath = "/path/to/proxy/dir"; - sinon.stub(toolcache, "find").returns(toolcachePath); - - const features = mockOfflineFeatures(tempDir, logger); - const path = await startProxyExports.getProxyBinaryPath(logger, features); - - t.assert(path); - t.is( - path, - filepath.join(toolcachePath, startProxyExports.getProxyFilename()), - ); - }); -}); - -test("getProxyBinaryPath - downloads proxy if not in cache", async (t) => { - const logger = new RecordingLogger(); - const downloadUrl = "url-we-want"; - mockGetReleaseByTag([ - { name: startProxyExports.getProxyPackage(), url: downloadUrl }, - ]); - - const toolcachePath = "/path/to/proxy/dir"; - const find = sinon.stub(toolcache, "find").returns(""); - const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({ - auth: "", - url: "", - apiURL: "", - }); - const getAuthorizationHeaderFor = sinon - .stub(apiClient, "getAuthorizationHeaderFor") - .returns(undefined); - const archivePath = "/path/to/archive"; - const downloadTool = sinon - .stub(toolcache, "downloadTool") - .resolves(archivePath); - const extractedPath = "/path/to/extracted"; - const extractTar = sinon - .stub(toolcache, "extractTar") - .resolves(extractedPath); - const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath); - - const path = await startProxyExports.getProxyBinaryPath( - logger, - createFeatures([]), - ); +test.serial( + "getProxyBinaryPath - returns path from tool cache if available", + async (t) => { + const logger = new RecordingLogger(); + mockGetReleaseByTag(); - t.assert(find.calledOnce); - t.assert(getApiDetails.calledOnce); - t.assert(getAuthorizationHeaderFor.calledOnce); - t.assert(downloadTool.calledOnceWith(downloadUrl)); - t.assert(extractTar.calledOnceWith(archivePath)); - t.assert(cacheDir.calledOnceWith(extractedPath)); - t.assert(path); - t.is( - path, - filepath.join(toolcachePath, startProxyExports.getProxyFilename()), - ); + await withTmpDir(async (tempDir) => { + const toolcachePath = "/path/to/proxy/dir"; + sinon.stub(toolcache, "find").returns(toolcachePath); - checkExpectedLogMessages(t, logger.messages, [ - `Found '${startProxyExports.getProxyPackage()}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`, - ]); -}); + const features = mockOfflineFeatures(tempDir, logger); + const path = await startProxyExports.getProxyBinaryPath(logger, features); -test("getProxyBinaryPath - downloads proxy based on features if not in cache", async (t) => { - const logger = new RecordingLogger(); - const expectedTag = "codeql-bundle-v2.20.1"; - const expectedParams = { - owner: "github", - repo: "codeql-action", - tag: expectedTag, - }; - const downloadUrl = "url-we-want"; - const assets = [ - { - name: startProxyExports.getProxyPackage(), - url: downloadUrl, - }, - ]; + t.assert(path); + t.is( + path, + filepath.join(toolcachePath, startProxyExports.getProxyFilename()), + ); + }); + }, +); - const getReleaseByTag = sinon.stub(); - getReleaseByTag.withArgs(sinon.match(expectedParams)).resolves({ - status: 200, - data: { assets }, - headers: {}, - url: "GET /repos/:owner/:repo/releases/tags/:tag", - }); - mockGetApiClient({ repos: { getReleaseByTag } }); +test.serial( + "getProxyBinaryPath - downloads proxy if not in cache", + async (t) => { + const logger = new RecordingLogger(); + const downloadUrl = "url-we-want"; + mockGetReleaseByTag([ + { name: startProxyExports.getProxyPackage(), url: downloadUrl }, + ]); - await withTmpDir(async (tempDir) => { const toolcachePath = "/path/to/proxy/dir"; const find = sinon.stub(toolcache, "find").returns(""); const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({ @@ -742,40 +728,114 @@ test("getProxyBinaryPath - downloads proxy based on features if not in cache", a .resolves(extractedPath); const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath); - const gitHubVersion: GitHubVersion = { - type: GitHubVariant.DOTCOM, - }; - sinon.stub(apiClient, "getGitHubVersion").resolves(gitHubVersion); - - const features = setUpFeatureFlagTests(tempDir, logger, gitHubVersion); - sinon.stub(features, "getValue").callsFake(async (_feature, _codeql) => { - return true; - }); - const getDefaultCliVersion = sinon - .stub(features, "getDefaultCliVersion") - .resolves({ cliVersion: "2.20.1", tagName: expectedTag }); - const path = await startProxyExports.getProxyBinaryPath(logger, features); - - t.assert(getDefaultCliVersion.calledOnce); - sinon.assert.calledOnceWithMatch( - getReleaseByTag, - sinon.match(expectedParams), + const path = await startProxyExports.getProxyBinaryPath( + logger, + createFeatures([]), ); + t.assert(find.calledOnce); t.assert(getApiDetails.calledOnce); t.assert(getAuthorizationHeaderFor.calledOnce); t.assert(downloadTool.calledOnceWith(downloadUrl)); t.assert(extractTar.calledOnceWith(archivePath)); t.assert(cacheDir.calledOnceWith(extractedPath)); - t.assert(path); t.is( path, filepath.join(toolcachePath, startProxyExports.getProxyFilename()), ); - }); - checkExpectedLogMessages(t, logger.messages, [ - `Found '${startProxyExports.getProxyPackage()}' in release '${expectedTag}' at '${downloadUrl}'`, - ]); -}); + checkExpectedLogMessages(t, logger.messages, [ + `Found '${startProxyExports.getProxyPackage()}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`, + ]); + }, +); + +test.serial( + "getProxyBinaryPath - downloads proxy based on features if not in cache", + async (t) => { + const logger = new RecordingLogger(); + const expectedTag = "codeql-bundle-v2.20.1"; + const expectedParams = { + owner: "github", + repo: "codeql-action", + tag: expectedTag, + }; + const downloadUrl = "url-we-want"; + const assets = [ + { + name: startProxyExports.getProxyPackage(), + url: downloadUrl, + }, + ]; + + const getReleaseByTag = sinon.stub(); + getReleaseByTag.withArgs(sinon.match(expectedParams)).resolves({ + status: 200, + data: { assets }, + headers: {}, + url: "GET /repos/:owner/:repo/releases/tags/:tag", + }); + mockGetApiClient({ repos: { getReleaseByTag } }); + + await withTmpDir(async (tempDir) => { + const toolcachePath = "/path/to/proxy/dir"; + const find = sinon.stub(toolcache, "find").returns(""); + const getApiDetails = sinon.stub(apiClient, "getApiDetails").returns({ + auth: "", + url: "", + apiURL: "", + }); + const getAuthorizationHeaderFor = sinon + .stub(apiClient, "getAuthorizationHeaderFor") + .returns(undefined); + const archivePath = "/path/to/archive"; + const downloadTool = sinon + .stub(toolcache, "downloadTool") + .resolves(archivePath); + const extractedPath = "/path/to/extracted"; + const extractTar = sinon + .stub(toolcache, "extractTar") + .resolves(extractedPath); + const cacheDir = sinon + .stub(toolcache, "cacheDir") + .resolves(toolcachePath); + + const gitHubVersion: GitHubVersion = { + type: GitHubVariant.DOTCOM, + }; + sinon.stub(apiClient, "getGitHubVersion").resolves(gitHubVersion); + + const features = setUpFeatureFlagTests(tempDir, logger, gitHubVersion); + sinon.stub(features, "getValue").callsFake(async (_feature, _codeql) => { + return true; + }); + const getDefaultCliVersion = sinon + .stub(features, "getDefaultCliVersion") + .resolves({ cliVersion: "2.20.1", tagName: expectedTag }); + const path = await startProxyExports.getProxyBinaryPath(logger, features); + + t.assert(getDefaultCliVersion.calledOnce); + sinon.assert.calledOnceWithMatch( + getReleaseByTag, + sinon.match(expectedParams), + ); + t.assert(find.calledOnce); + t.assert(getApiDetails.calledOnce); + t.assert(getAuthorizationHeaderFor.calledOnce); + t.assert(downloadTool.calledOnceWith(downloadUrl)); + t.assert(extractTar.calledOnceWith(archivePath)); + t.assert(cacheDir.calledOnceWith(extractedPath)); + + t.assert(path); + t.is( + path, + filepath.join(toolcachePath, startProxyExports.getProxyFilename()), + ); + }); + + checkExpectedLogMessages(t, logger.messages, [ + `Found '${startProxyExports.getProxyPackage()}' in release '${expectedTag}' at '${downloadUrl}'`, + ]); + }, +); diff --git a/src/start-proxy/environment.test.ts b/src/start-proxy/environment.test.ts index 8dcb4c7b29..6722c53ab2 100644 --- a/src/start-proxy/environment.test.ts +++ b/src/start-proxy/environment.test.ts @@ -69,19 +69,22 @@ test("checkJavaEnvironment - none set", (t) => { assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, false); }); -test("checkJavaEnvironment - logs values when variables are set", (t) => { - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); +test.serial( + "checkJavaEnvironment - logs values when variables are set", + (t) => { + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); - for (const envVar of Object.values(JavaEnvVars)) { - process.env[envVar] = envVar; - } + for (const envVar of Object.values(JavaEnvVars)) { + process.env[envVar] = envVar; + } - checkJavaEnvVars(logger); - assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, true); -}); + checkJavaEnvVars(logger); + assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, true); + }, +); -test("discoverActionsJdks - discovers JDK paths", (t) => { +test.serial("discoverActionsJdks - discovers JDK paths", (t) => { // Clear GHA variables that may interfere with this test in CI. for (const envVar of Object.keys(process.env)) { if (envVar.startsWith("JAVA_HOME_")) { @@ -149,7 +152,7 @@ test("checkProxyEnvVars - none set", (t) => { assertEnvVarLogMessages(t, Object.values(ProxyEnvVars), messages, false); }); -test("checkProxyEnvVars - logs values when variables are set", (t) => { +test.serial("checkProxyEnvVars - logs values when variables are set", (t) => { const messages: LoggedMessage[] = []; const logger = getRecordingLogger(messages); @@ -161,7 +164,7 @@ test("checkProxyEnvVars - logs values when variables are set", (t) => { assertEnvVarLogMessages(t, Object.values(ProxyEnvVars), messages, true); }); -test("checkProxyEnvVars - credentials are removed from URLs", (t) => { +test.serial("checkProxyEnvVars - credentials are removed from URLs", (t) => { const messages: LoggedMessage[] = []; const logger = getRecordingLogger(messages); @@ -178,36 +181,45 @@ test("checkProxyEnvVars - credentials are removed from URLs", (t) => { ); }); -test("checkProxyEnvironment - includes base checks for all known languages", async (t) => { - stubToolrunner(); +test.serial( + "checkProxyEnvironment - includes base checks for all known languages", + async (t) => { + stubToolrunner(); - for (const language of Object.values(KnownLanguage)) { - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); + for (const language of Object.values(KnownLanguage)) { + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); - await checkProxyEnvironment(logger, language); - assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false); - } -}); + await checkProxyEnvironment(logger, language); + assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false); + } + }, +); -test("checkProxyEnvironment - includes Java checks for Java", async (t) => { - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); +test.serial( + "checkProxyEnvironment - includes Java checks for Java", + async (t) => { + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); - stubToolrunner(); + stubToolrunner(); - await checkProxyEnvironment(logger, KnownLanguage.java); - assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false); - assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, false); -}); + await checkProxyEnvironment(logger, KnownLanguage.java); + assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false); + assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, false); + }, +); -test("checkProxyEnvironment - includes language-specific checks if the language is undefined", async (t) => { - const messages: LoggedMessage[] = []; - const logger = getRecordingLogger(messages); +test.serial( + "checkProxyEnvironment - includes language-specific checks if the language is undefined", + async (t) => { + const messages: LoggedMessage[] = []; + const logger = getRecordingLogger(messages); - stubToolrunner(); + stubToolrunner(); - await checkProxyEnvironment(logger, undefined); - assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false); - assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, false); -}); + await checkProxyEnvironment(logger, undefined); + assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false); + assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, false); + }, +); diff --git a/src/status-report.test.ts b/src/status-report.test.ts index 6f4bb11f89..35d608b7d3 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -38,7 +38,7 @@ function setupEnvironmentAndStub(tmpDir: string) { getRequiredInput.withArgs("matrix").resolves("input/matrix"); } -test("createStatusReportBase", async (t) => { +test.serial("createStatusReportBase", async (t) => { await withTmpDir(async (tmpDir: string) => { setupEnvironmentAndStub(tmpDir); @@ -88,7 +88,7 @@ test("createStatusReportBase", async (t) => { }); }); -test("createStatusReportBase - empty configuration", async (t) => { +test.serial("createStatusReportBase - empty configuration", async (t) => { await withTmpDir(async (tmpDir: string) => { setupEnvironmentAndStub(tmpDir); @@ -108,7 +108,7 @@ test("createStatusReportBase - empty configuration", async (t) => { }); }); -test("createStatusReportBase - partial configuration", async (t) => { +test.serial("createStatusReportBase - partial configuration", async (t) => { await withTmpDir(async (tmpDir: string) => { setupEnvironmentAndStub(tmpDir); @@ -131,7 +131,7 @@ test("createStatusReportBase - partial configuration", async (t) => { }); }); -test("createStatusReportBase_firstParty", async (t) => { +test.serial("createStatusReportBase_firstParty", async (t) => { await withTmpDir(async (tmpDir: string) => { setupEnvironmentAndStub(tmpDir); @@ -235,58 +235,61 @@ test("createStatusReportBase_firstParty", async (t) => { }); }); -test("getActionStatus handling correctly various types of errors", (t) => { - t.is( - getActionsStatus(new Error("arbitrary error")), - "failure", - "We categorise an arbitrary error as a failure", - ); - - t.is( - getActionsStatus(new ConfigurationError("arbitrary error")), - "user-error", - "We categorise a ConfigurationError as a user error", - ); - - t.is( - getActionsStatus(new Error("exit code 1"), "multiple things went wrong"), - "failure", - "getActionsStatus should return failure if passed an arbitrary error and an additional failure cause", - ); - - t.is( - getActionsStatus( - new ConfigurationError("exit code 1"), - "multiple things went wrong", - ), - "user-error", - "getActionsStatus should return user-error if passed a configuration error and an additional failure cause", - ); - - t.is( - getActionsStatus(), - "success", - "getActionsStatus should return success if no error is passed", - ); - - t.is( - getActionsStatus(new Object()), - "failure", - "getActionsStatus should return failure if passed an arbitrary object", - ); - - t.is( - getActionsStatus(null, "an error occurred"), - "failure", - "getActionsStatus should return failure if passed null and an additional failure cause", - ); - - t.is( - getActionsStatus(wrapError(new ConfigurationError("arbitrary error"))), - "user-error", - "We still recognise a wrapped ConfigurationError as a user error", - ); -}); +test.serial( + "getActionStatus handling correctly various types of errors", + (t) => { + t.is( + getActionsStatus(new Error("arbitrary error")), + "failure", + "We categorise an arbitrary error as a failure", + ); + + t.is( + getActionsStatus(new ConfigurationError("arbitrary error")), + "user-error", + "We categorise a ConfigurationError as a user error", + ); + + t.is( + getActionsStatus(new Error("exit code 1"), "multiple things went wrong"), + "failure", + "getActionsStatus should return failure if passed an arbitrary error and an additional failure cause", + ); + + t.is( + getActionsStatus( + new ConfigurationError("exit code 1"), + "multiple things went wrong", + ), + "user-error", + "getActionsStatus should return user-error if passed a configuration error and an additional failure cause", + ); + + t.is( + getActionsStatus(), + "success", + "getActionsStatus should return success if no error is passed", + ); + + t.is( + getActionsStatus(new Object()), + "failure", + "getActionsStatus should return failure if passed an arbitrary object", + ); + + t.is( + getActionsStatus(null, "an error occurred"), + "failure", + "getActionsStatus should return failure if passed null and an additional failure cause", + ); + + t.is( + getActionsStatus(wrapError(new ConfigurationError("arbitrary error"))), + "user-error", + "We still recognise a wrapped ConfigurationError as a user error", + ); + }, +); const testCreateInitWithConfigStatusReport = test.macro({ exec: async ( @@ -337,7 +340,7 @@ const testCreateInitWithConfigStatusReport = test.macro({ title: (_, title) => `createInitWithConfigStatusReport: ${title}`, }); -test( +test.serial( testCreateInitWithConfigStatusReport, "returns a value", createTestConfig({ @@ -352,7 +355,7 @@ test( }, ); -test( +test.serial( testCreateInitWithConfigStatusReport, "includes packs for a single language", createTestConfig({ @@ -369,7 +372,7 @@ test( }, ); -test( +test.serial( testCreateInitWithConfigStatusReport, "includes packs for multiple languages", createTestConfig({ diff --git a/src/trap-caching.test.ts b/src/trap-caching.test.ts index 66913d61b9..a6c7fc76cb 100644 --- a/src/trap-caching.test.ts +++ b/src/trap-caching.test.ts @@ -94,7 +94,7 @@ function getTestConfigWithTempDir(tempDir: string): configUtils.Config { }); } -test("check flags for JS, analyzing default branch", async (t) => { +test.serial("check flags for JS, analyzing default branch", async (t) => { await util.withTmpDir(async (tmpDir) => { const config = getTestConfigWithTempDir(tmpDir); sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); @@ -110,7 +110,7 @@ test("check flags for JS, analyzing default branch", async (t) => { }); }); -test("check flags for all, not analyzing default branch", async (t) => { +test.serial("check flags for all, not analyzing default branch", async (t) => { await util.withTmpDir(async (tmpDir) => { const config = getTestConfigWithTempDir(tmpDir); sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); @@ -137,7 +137,7 @@ test("get languages that support TRAP caching", async (t) => { t.deepEqual(languagesSupportingCaching, [KnownLanguage.javascript]); }); -test("upload cache key contains right fields", async (t) => { +test.serial("upload cache key contains right fields", async (t) => { const loggedMessages = []; const logger = getRecordingLogger(loggedMessages); sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); @@ -156,47 +156,50 @@ test("upload cache key contains right fields", async (t) => { ); }); -test("download cache looks for the right key and creates dir", async (t) => { - await util.withTmpDir(async (tmpDir) => { - const loggedMessages = []; - const logger = getRecordingLogger(loggedMessages); - sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tmpDir); - sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); - const stubRestore = sinon.stub(cache, "restoreCache").resolves("found"); - const eventFile = path.resolve(tmpDir, "event.json"); - process.env.GITHUB_EVENT_NAME = "pull_request"; - process.env.GITHUB_EVENT_PATH = eventFile; - fs.writeFileSync( - eventFile, - JSON.stringify({ - pull_request: { - base: { - sha: "somesha", +test.serial( + "download cache looks for the right key and creates dir", + async (t) => { + await util.withTmpDir(async (tmpDir) => { + const loggedMessages = []; + const logger = getRecordingLogger(loggedMessages); + sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tmpDir); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); + const stubRestore = sinon.stub(cache, "restoreCache").resolves("found"); + const eventFile = path.resolve(tmpDir, "event.json"); + process.env.GITHUB_EVENT_NAME = "pull_request"; + process.env.GITHUB_EVENT_PATH = eventFile; + fs.writeFileSync( + eventFile, + JSON.stringify({ + pull_request: { + base: { + sha: "somesha", + }, }, - }, - }), - ); - await downloadTrapCaches( - stubCodeql, - [KnownLanguage.javascript, KnownLanguage.cpp], - logger, - ); - t.assert( - stubRestore.calledOnceWith( - sinon.match.array.contains([ - path.resolve(tmpDir, "trapCaches", "javascript"), - ]), - sinon - .match("somesha") - .and(sinon.match("2.10.3")) - .and(sinon.match("javascript")), - ), - ); - t.assert(fs.existsSync(path.resolve(tmpDir, "trapCaches", "javascript"))); - }); -}); + }), + ); + await downloadTrapCaches( + stubCodeql, + [KnownLanguage.javascript, KnownLanguage.cpp], + logger, + ); + t.assert( + stubRestore.calledOnceWith( + sinon.match.array.contains([ + path.resolve(tmpDir, "trapCaches", "javascript"), + ]), + sinon + .match("somesha") + .and(sinon.match("2.10.3")) + .and(sinon.match("javascript")), + ), + ); + t.assert(fs.existsSync(path.resolve(tmpDir, "trapCaches", "javascript"))); + }); + }, +); -test("cleanup removes only old CodeQL TRAP caches", async (t) => { +test.serial("cleanup removes only old CodeQL TRAP caches", async (t) => { await util.withTmpDir(async (tmpDir) => { // This config specifies that we are analyzing JavaScript and Ruby, but not Swift. const config = getTestConfigWithTempDir(tmpDir); diff --git a/src/upload-lib.test.ts b/src/upload-lib.test.ts index 973ee81905..92dc2e7732 100644 --- a/src/upload-lib.test.ts +++ b/src/upload-lib.test.ts @@ -22,7 +22,7 @@ test.beforeEach(() => { initializeEnvironment("1.2.3"); }); -test("validateSarifFileSchema - valid", (t) => { +test.serial("validateSarifFileSchema - valid", (t) => { const inputFile = `${__dirname}/../src/testdata/valid-sarif.sarif`; t.notThrows(() => uploadLib.validateSarifFileSchema( @@ -33,7 +33,7 @@ test("validateSarifFileSchema - valid", (t) => { ); }); -test("validateSarifFileSchema - invalid", (t) => { +test.serial("validateSarifFileSchema - invalid", (t) => { const inputFile = `${__dirname}/../src/testdata/invalid-sarif.sarif`; t.throws(() => uploadLib.validateSarifFileSchema( @@ -44,69 +44,72 @@ test("validateSarifFileSchema - invalid", (t) => { ); }); -test("validate correct payload used for push, PR merge commit, and PR head", async (t) => { - process.env["GITHUB_EVENT_NAME"] = "push"; - const pushPayload: any = uploadLib.buildPayload( - "commit", - "refs/heads/master", - "key", - undefined, - "", - 1234, - 1, - "/opt/src", - undefined, - ["CodeQL", "eslint"], - "mergeBaseCommit", - ); - // Not triggered by a pull request - t.falsy(pushPayload.base_ref); - t.falsy(pushPayload.base_sha); - - process.env["GITHUB_EVENT_NAME"] = "pull_request"; - process.env["GITHUB_SHA"] = "commit"; - process.env["GITHUB_BASE_REF"] = "master"; - process.env["GITHUB_EVENT_PATH"] = - `${__dirname}/../src/testdata/pull_request.json`; - const prMergePayload: any = uploadLib.buildPayload( - "commit", - "refs/pull/123/merge", - "key", - undefined, - "", - 1234, - 1, - "/opt/src", - undefined, - ["CodeQL", "eslint"], - "mergeBaseCommit", - ); - // Uploads for a merge commit use the merge base - t.deepEqual(prMergePayload.base_ref, "refs/heads/master"); - t.deepEqual(prMergePayload.base_sha, "mergeBaseCommit"); - - const prHeadPayload: any = uploadLib.buildPayload( - "headCommit", - "refs/pull/123/head", - "key", - undefined, - "", - 1234, - 1, - "/opt/src", - undefined, - ["CodeQL", "eslint"], - "mergeBaseCommit", - ); - // Uploads for the head use the PR base - t.deepEqual(prHeadPayload.base_ref, "refs/heads/master"); - t.deepEqual( - prHeadPayload.base_sha, - "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", - ); -}); +test.serial( + "validate correct payload used for push, PR merge commit, and PR head", + async (t) => { + process.env["GITHUB_EVENT_NAME"] = "push"; + const pushPayload: any = uploadLib.buildPayload( + "commit", + "refs/heads/master", + "key", + undefined, + "", + 1234, + 1, + "/opt/src", + undefined, + ["CodeQL", "eslint"], + "mergeBaseCommit", + ); + // Not triggered by a pull request + t.falsy(pushPayload.base_ref); + t.falsy(pushPayload.base_sha); + + process.env["GITHUB_EVENT_NAME"] = "pull_request"; + process.env["GITHUB_SHA"] = "commit"; + process.env["GITHUB_BASE_REF"] = "master"; + process.env["GITHUB_EVENT_PATH"] = + `${__dirname}/../src/testdata/pull_request.json`; + const prMergePayload: any = uploadLib.buildPayload( + "commit", + "refs/pull/123/merge", + "key", + undefined, + "", + 1234, + 1, + "/opt/src", + undefined, + ["CodeQL", "eslint"], + "mergeBaseCommit", + ); + // Uploads for a merge commit use the merge base + t.deepEqual(prMergePayload.base_ref, "refs/heads/master"); + t.deepEqual(prMergePayload.base_sha, "mergeBaseCommit"); + + const prHeadPayload: any = uploadLib.buildPayload( + "headCommit", + "refs/pull/123/head", + "key", + undefined, + "", + 1234, + 1, + "/opt/src", + undefined, + ["CodeQL", "eslint"], + "mergeBaseCommit", + ); + // Uploads for the head use the PR base + t.deepEqual(prHeadPayload.base_ref, "refs/heads/master"); + t.deepEqual( + prHeadPayload.base_sha, + "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", + ); + }, +); -test("finding SARIF files", async (t) => { +test.serial("finding SARIF files", async (t) => { await withTmpDir(async (tmpDir) => { // include a couple of sarif files fs.writeFileSync(path.join(tmpDir, "a.sarif"), ""); @@ -190,7 +193,7 @@ test("finding SARIF files", async (t) => { }); }); -test("getGroupedSarifFilePaths - Risk Assessment files", async (t) => { +test.serial("getGroupedSarifFilePaths - Risk Assessment files", async (t) => { await withTmpDir(async (tmpDir) => { const sarifPath = path.join(tmpDir, "a.csra.sarif"); fs.writeFileSync(sarifPath, ""); @@ -208,7 +211,7 @@ test("getGroupedSarifFilePaths - Risk Assessment files", async (t) => { }); }); -test("getGroupedSarifFilePaths - Code Quality file", async (t) => { +test.serial("getGroupedSarifFilePaths - Code Quality file", async (t) => { await withTmpDir(async (tmpDir) => { const sarifPath = path.join(tmpDir, "a.quality.sarif"); fs.writeFileSync(sarifPath, ""); @@ -226,7 +229,7 @@ test("getGroupedSarifFilePaths - Code Quality file", async (t) => { }); }); -test("getGroupedSarifFilePaths - Code Scanning file", async (t) => { +test.serial("getGroupedSarifFilePaths - Code Scanning file", async (t) => { await withTmpDir(async (tmpDir) => { const sarifPath = path.join(tmpDir, "a.sarif"); fs.writeFileSync(sarifPath, ""); @@ -244,7 +247,7 @@ test("getGroupedSarifFilePaths - Code Scanning file", async (t) => { }); }); -test("getGroupedSarifFilePaths - Other file", async (t) => { +test.serial("getGroupedSarifFilePaths - Other file", async (t) => { await withTmpDir(async (tmpDir) => { const sarifPath = path.join(tmpDir, "a.json"); fs.writeFileSync(sarifPath, ""); @@ -262,7 +265,7 @@ test("getGroupedSarifFilePaths - Other file", async (t) => { }); }); -test("populateRunAutomationDetails", (t) => { +test.serial("populateRunAutomationDetails", (t) => { const tool = { driver: { name: "test tool" } }; let sarifLog: sarif.Log = { version: "2.1.0", @@ -338,7 +341,7 @@ test("populateRunAutomationDetails", (t) => { t.deepEqual(modifiedSarif, expectedSarif); }); -test("validateUniqueCategory when empty", (t) => { +test.serial("validateUniqueCategory when empty", (t) => { t.notThrows(() => uploadLib.validateUniqueCategory( createMockSarif(), @@ -353,7 +356,7 @@ test("validateUniqueCategory when empty", (t) => { ); }); -test("validateUniqueCategory for automation details id", (t) => { +test.serial("validateUniqueCategory for automation details id", (t) => { t.notThrows(() => uploadLib.validateUniqueCategory( createMockSarif("abc"), @@ -422,7 +425,7 @@ test("validateUniqueCategory for automation details id", (t) => { ); }); -test("validateUniqueCategory for tool name", (t) => { +test.serial("validateUniqueCategory for tool name", (t) => { t.notThrows(() => uploadLib.validateUniqueCategory( createMockSarif(undefined, "abc"), @@ -491,77 +494,80 @@ test("validateUniqueCategory for tool name", (t) => { ); }); -test("validateUniqueCategory for automation details id and tool name", (t) => { - t.notThrows(() => - uploadLib.validateUniqueCategory( - createMockSarif("abc", "abc"), - CodeScanning.sentinelPrefix, - ), - ); - t.throws(() => - uploadLib.validateUniqueCategory( - createMockSarif("abc", "abc"), - CodeScanning.sentinelPrefix, - ), - ); +test.serial( + "validateUniqueCategory for automation details id and tool name", + (t) => { + t.notThrows(() => + uploadLib.validateUniqueCategory( + createMockSarif("abc", "abc"), + CodeScanning.sentinelPrefix, + ), + ); + t.throws(() => + uploadLib.validateUniqueCategory( + createMockSarif("abc", "abc"), + CodeScanning.sentinelPrefix, + ), + ); - t.notThrows(() => - uploadLib.validateUniqueCategory( - createMockSarif("abc_", "def"), - CodeScanning.sentinelPrefix, - ), - ); - t.throws(() => - uploadLib.validateUniqueCategory( - createMockSarif("abc_", "def"), - CodeScanning.sentinelPrefix, - ), - ); + t.notThrows(() => + uploadLib.validateUniqueCategory( + createMockSarif("abc_", "def"), + CodeScanning.sentinelPrefix, + ), + ); + t.throws(() => + uploadLib.validateUniqueCategory( + createMockSarif("abc_", "def"), + CodeScanning.sentinelPrefix, + ), + ); - t.notThrows(() => - uploadLib.validateUniqueCategory( - createMockSarif("ghi", "_jkl"), - CodeScanning.sentinelPrefix, - ), - ); - t.throws(() => - uploadLib.validateUniqueCategory( - createMockSarif("ghi", "_jkl"), - CodeScanning.sentinelPrefix, - ), - ); + t.notThrows(() => + uploadLib.validateUniqueCategory( + createMockSarif("ghi", "_jkl"), + CodeScanning.sentinelPrefix, + ), + ); + t.throws(() => + uploadLib.validateUniqueCategory( + createMockSarif("ghi", "_jkl"), + CodeScanning.sentinelPrefix, + ), + ); - // Our category sanitization is not perfect. Here are some examples - // of where we see false clashes because we replace some characters - // with `_` in `sanitize`. - t.notThrows(() => - uploadLib.validateUniqueCategory( - createMockSarif("abc", "def__"), - CodeScanning.sentinelPrefix, - ), - ); - t.throws(() => - uploadLib.validateUniqueCategory( - createMockSarif("abc_def", "_"), - CodeScanning.sentinelPrefix, - ), - ); + // Our category sanitization is not perfect. Here are some examples + // of where we see false clashes because we replace some characters + // with `_` in `sanitize`. + t.notThrows(() => + uploadLib.validateUniqueCategory( + createMockSarif("abc", "def__"), + CodeScanning.sentinelPrefix, + ), + ); + t.throws(() => + uploadLib.validateUniqueCategory( + createMockSarif("abc_def", "_"), + CodeScanning.sentinelPrefix, + ), + ); - t.notThrows(() => - uploadLib.validateUniqueCategory( - createMockSarif("mno_", "pqr"), - CodeScanning.sentinelPrefix, - ), - ); - t.throws(() => - uploadLib.validateUniqueCategory( - createMockSarif("mno", "_pqr"), - CodeScanning.sentinelPrefix, - ), - ); -}); + t.notThrows(() => + uploadLib.validateUniqueCategory( + createMockSarif("mno_", "pqr"), + CodeScanning.sentinelPrefix, + ), + ); + t.throws(() => + uploadLib.validateUniqueCategory( + createMockSarif("mno", "_pqr"), + CodeScanning.sentinelPrefix, + ), + ); + }, +); -test("validateUniqueCategory for multiple runs", (t) => { +test.serial("validateUniqueCategory for multiple runs", (t) => { const sarif1 = createMockSarif("abc", "def"); const sarif2 = createMockSarif("ghi", "jkl"); @@ -583,7 +589,7 @@ test("validateUniqueCategory for multiple runs", (t) => { ); }); -test("validateUniqueCategory with different prefixes", (t) => { +test.serial("validateUniqueCategory with different prefixes", (t) => { t.notThrows(() => uploadLib.validateUniqueCategory( createMockSarif(), @@ -598,7 +604,7 @@ test("validateUniqueCategory with different prefixes", (t) => { ); }); -test("accept results with invalid artifactLocation.uri value", (t) => { +test.serial("accept results with invalid artifactLocation.uri value", (t) => { const loggedMessages: string[] = []; const mockLogger = { info: (message: string) => { @@ -621,100 +627,124 @@ test("accept results with invalid artifactLocation.uri value", (t) => { ); }); -test("shouldShowCombineSarifFilesDeprecationWarning when on dotcom", async (t) => { - t.true( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.13", async (t) => { - t.false( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], - { - type: GitHubVariant.GHES, - version: "3.13.2", - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.14", async (t) => { - t.true( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], - { - type: GitHubVariant.GHES, - version: "3.14.0", - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.16 pre", async (t) => { - t.true( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], - { - type: GitHubVariant.GHES, - version: "3.16.0.pre1", - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning with only 1 run", async (t) => { - t.false( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning with distinct categories", async (t) => { - t.false( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def"), createMockSarif("def", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning with distinct tools", async (t) => { - t.false( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "abc"), createMockSarif("abc", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); - -test("shouldShowCombineSarifFilesDeprecationWarning when environment variable is already set", async (t) => { - process.env["CODEQL_MERGE_SARIF_DEPRECATION_WARNING"] = "true"; - - t.false( - await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning when on dotcom", + async (t) => { + t.true( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.13", + async (t) => { + t.false( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.GHES, + version: "3.13.2", + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.14", + async (t) => { + t.true( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.GHES, + version: "3.14.0", + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.16 pre", + async (t) => { + t.true( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.GHES, + version: "3.16.0.pre1", + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning with only 1 run", + async (t) => { + t.false( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning with distinct categories", + async (t) => { + t.false( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def"), createMockSarif("def", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning with distinct tools", + async (t) => { + t.false( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "abc"), createMockSarif("abc", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); + +test.serial( + "shouldShowCombineSarifFilesDeprecationWarning when environment variable is already set", + async (t) => { + process.env["CODEQL_MERGE_SARIF_DEPRECATION_WARNING"] = "true"; + + t.false( + await uploadLib.shouldShowCombineSarifFilesDeprecationWarning( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); -test("throwIfCombineSarifFilesDisabled when on dotcom", async (t) => { +test.serial("throwIfCombineSarifFilesDisabled when on dotcom", async (t) => { await t.throwsAsync( uploadLib.throwIfCombineSarifFilesDisabled( [createMockSarif("abc", "def"), createMockSarif("abc", "def")], @@ -729,7 +759,7 @@ test("throwIfCombineSarifFilesDisabled when on dotcom", async (t) => { ); }); -test("throwIfCombineSarifFilesDisabled when on GHES 3.13", async (t) => { +test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.13", async (t) => { await t.notThrowsAsync( uploadLib.throwIfCombineSarifFilesDisabled( [createMockSarif("abc", "def"), createMockSarif("abc", "def")], @@ -741,7 +771,7 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.13", async (t) => { ); }); -test("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => { +test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => { await t.notThrowsAsync( uploadLib.throwIfCombineSarifFilesDisabled( [createMockSarif("abc", "def"), createMockSarif("abc", "def")], @@ -753,7 +783,7 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => { ); }); -test("throwIfCombineSarifFilesDisabled when on GHES 3.17", async (t) => { +test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.17", async (t) => { await t.notThrowsAsync( uploadLib.throwIfCombineSarifFilesDisabled( [createMockSarif("abc", "def"), createMockSarif("abc", "def")], @@ -765,39 +795,45 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.17", async (t) => { ); }); -test("throwIfCombineSarifFilesDisabled when on GHES 3.18 pre", async (t) => { - await t.throwsAsync( - uploadLib.throwIfCombineSarifFilesDisabled( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], +test.serial( + "throwIfCombineSarifFilesDisabled when on GHES 3.18 pre", + async (t) => { + await t.throwsAsync( + uploadLib.throwIfCombineSarifFilesDisabled( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.GHES, + version: "3.18.0.pre1", + }, + ), { - type: GitHubVariant.GHES, - version: "3.18.0.pre1", + message: + /The CodeQL Action does not support uploading multiple SARIF runs with the same category/, }, - ), - { - message: - /The CodeQL Action does not support uploading multiple SARIF runs with the same category/, - }, - ); -}); - -test("throwIfCombineSarifFilesDisabled when on GHES 3.18 alpha", async (t) => { - await t.throwsAsync( - uploadLib.throwIfCombineSarifFilesDisabled( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + ); + }, +); + +test.serial( + "throwIfCombineSarifFilesDisabled when on GHES 3.18 alpha", + async (t) => { + await t.throwsAsync( + uploadLib.throwIfCombineSarifFilesDisabled( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.GHES, + version: "3.18.0-alpha.1", + }, + ), { - type: GitHubVariant.GHES, - version: "3.18.0-alpha.1", + message: + /The CodeQL Action does not support uploading multiple SARIF runs with the same category/, }, - ), - { - message: - /The CodeQL Action does not support uploading multiple SARIF runs with the same category/, - }, - ); -}); + ); + }, +); -test("throwIfCombineSarifFilesDisabled when on GHES 3.18", async (t) => { +test.serial("throwIfCombineSarifFilesDisabled when on GHES 3.18", async (t) => { await t.throwsAsync( uploadLib.throwIfCombineSarifFilesDisabled( [createMockSarif("abc", "def"), createMockSarif("abc", "def")], @@ -813,19 +849,22 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.18", async (t) => { ); }); -test("throwIfCombineSarifFilesDisabled with an invalid GHES version", async (t) => { - await t.notThrowsAsync( - uploadLib.throwIfCombineSarifFilesDisabled( - [createMockSarif("abc", "def"), createMockSarif("abc", "def")], - { - type: GitHubVariant.GHES, - version: "foobar", - }, - ), - ); -}); +test.serial( + "throwIfCombineSarifFilesDisabled with an invalid GHES version", + async (t) => { + await t.notThrowsAsync( + uploadLib.throwIfCombineSarifFilesDisabled( + [createMockSarif("abc", "def"), createMockSarif("abc", "def")], + { + type: GitHubVariant.GHES, + version: "foobar", + }, + ), + ); + }, +); -test("throwIfCombineSarifFilesDisabled with only 1 run", async (t) => { +test.serial("throwIfCombineSarifFilesDisabled with only 1 run", async (t) => { await t.notThrowsAsync( uploadLib.throwIfCombineSarifFilesDisabled( [createMockSarif("abc", "def")], @@ -836,68 +875,80 @@ test("throwIfCombineSarifFilesDisabled with only 1 run", async (t) => { ); }); -test("throwIfCombineSarifFilesDisabled with distinct categories", async (t) => { - await t.notThrowsAsync( - uploadLib.throwIfCombineSarifFilesDisabled( - [createMockSarif("abc", "def"), createMockSarif("def", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); +test.serial( + "throwIfCombineSarifFilesDisabled with distinct categories", + async (t) => { + await t.notThrowsAsync( + uploadLib.throwIfCombineSarifFilesDisabled( + [createMockSarif("abc", "def"), createMockSarif("def", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); + +test.serial( + "throwIfCombineSarifFilesDisabled with distinct tools", + async (t) => { + await t.notThrowsAsync( + uploadLib.throwIfCombineSarifFilesDisabled( + [createMockSarif("abc", "abc"), createMockSarif("abc", "def")], + { + type: GitHubVariant.DOTCOM, + }, + ), + ); + }, +); + +test.serial( + "shouldConsiderConfigurationError correctly detects configuration errors", + (t) => { + const error1 = [ + "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled", + ]; + t.true(uploadLib.shouldConsiderConfigurationError(error1)); -test("throwIfCombineSarifFilesDisabled with distinct tools", async (t) => { - await t.notThrowsAsync( - uploadLib.throwIfCombineSarifFilesDisabled( - [createMockSarif("abc", "abc"), createMockSarif("abc", "def")], - { - type: GitHubVariant.DOTCOM, - }, - ), - ); -}); + const error2 = [ + "rejecting delivery as the repository has too many logical alerts", + ]; + t.true(uploadLib.shouldConsiderConfigurationError(error2)); -test("shouldConsiderConfigurationError correctly detects configuration errors", (t) => { - const error1 = [ - "CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled", - ]; - t.true(uploadLib.shouldConsiderConfigurationError(error1)); - - const error2 = [ - "rejecting delivery as the repository has too many logical alerts", - ]; - t.true(uploadLib.shouldConsiderConfigurationError(error2)); - - // We fail cases where we get > 1 error messages back - const error3 = [ - "rejecting delivery as the repository has too many alerts", - "extra error message", - ]; - t.false(uploadLib.shouldConsiderConfigurationError(error3)); -}); + // We fail cases where we get > 1 error messages back + const error3 = [ + "rejecting delivery as the repository has too many alerts", + "extra error message", + ]; + t.false(uploadLib.shouldConsiderConfigurationError(error3)); + }, +); + +test.serial( + "shouldConsiderInvalidRequest returns correct recognises processing errors", + (t) => { + const error1 = [ + "rejecting SARIF", + "an invalid URI was provided as a SARIF location", + ]; + t.true(uploadLib.shouldConsiderInvalidRequest(error1)); -test("shouldConsiderInvalidRequest returns correct recognises processing errors", (t) => { - const error1 = [ - "rejecting SARIF", - "an invalid URI was provided as a SARIF location", - ]; - t.true(uploadLib.shouldConsiderInvalidRequest(error1)); - - const error2 = [ - "locationFromSarifResult: expected artifact location", - "an invalid URI was provided as a SARIF location", - ]; - t.true(uploadLib.shouldConsiderInvalidRequest(error2)); - - // We expect ALL errors to be of processing errors, for the outcome to be classified as - // an invalid SARIF upload error. - const error3 = [ - "could not convert rules: invalid security severity value, is not a number", - "an unknown error occurred", - ]; - t.false(uploadLib.shouldConsiderInvalidRequest(error3)); -}); + const error2 = [ + "locationFromSarifResult: expected artifact location", + "an invalid URI was provided as a SARIF location", + ]; + t.true(uploadLib.shouldConsiderInvalidRequest(error2)); + + // We expect ALL errors to be of processing errors, for the outcome to be classified as + // an invalid SARIF upload error. + const error3 = [ + "could not convert rules: invalid security severity value, is not a number", + "an unknown error occurred", + ]; + t.false(uploadLib.shouldConsiderInvalidRequest(error3)); + }, +); function createMockSarif(id?: string, tool?: string): sarif.Log { return { @@ -962,55 +1013,70 @@ function uploadPayloadFixtures(analysis: analyses.AnalysisConfig) { for (const analysisKind of analyses.supportedAnalysisKinds) { const analysis = analyses.getAnalysisConfig(analysisKind); - test(`uploadPayload on ${analysis.name} uploads successfully`, async (t) => { - const { upload, requestStub, mockData } = uploadPayloadFixtures(analysis); - requestStub - .withArgs(analysis.target, { - owner: mockData.owner, - repo: mockData.repo, - data: mockData.payload, - }) - .onFirstCall() - .returns(Promise.resolve(mockData.response)); - const result = await upload(); - t.is(result, mockData.response.data.id); - t.true(requestStub.calledOnce); - }); + test.serial( + `uploadPayload on ${analysis.name} uploads successfully`, + async (t) => { + const { upload, requestStub, mockData } = uploadPayloadFixtures(analysis); + requestStub + .withArgs(analysis.target, { + owner: mockData.owner, + repo: mockData.repo, + data: mockData.payload, + }) + .onFirstCall() + .returns(Promise.resolve(mockData.response)); + const result = await upload(); + t.is(result, mockData.response.data.id); + t.true(requestStub.calledOnce); + }, + ); for (const envVar of [ "CODEQL_ACTION_SKIP_SARIF_UPLOAD", "CODEQL_ACTION_TEST_MODE", ]) { - test(`uploadPayload on ${analysis.name} skips upload when ${envVar} is set`, async (t) => { - const { upload, requestStub, mockData } = uploadPayloadFixtures(analysis); - await withTmpDir(async (tmpDir) => { - process.env.RUNNER_TEMP = tmpDir; - process.env[envVar] = "true"; - const result = await upload(); - t.is(result, "dummy-sarif-id"); - t.false(requestStub.called); - - const payloadFile = path.join(tmpDir, `payload-${analysis.kind}.json`); - t.true(fs.existsSync(payloadFile)); - - const savedPayload = JSON.parse(fs.readFileSync(payloadFile, "utf8")); - t.deepEqual(savedPayload, mockData.payload); - }); - }); + test.serial( + `uploadPayload on ${analysis.name} skips upload when ${envVar} is set`, + async (t) => { + const { upload, requestStub, mockData } = + uploadPayloadFixtures(analysis); + await withTmpDir(async (tmpDir) => { + process.env.RUNNER_TEMP = tmpDir; + process.env[envVar] = "true"; + const result = await upload(); + t.is(result, "dummy-sarif-id"); + t.false(requestStub.called); + + const payloadFile = path.join( + tmpDir, + `payload-${analysis.kind}.json`, + ); + t.true(fs.existsSync(payloadFile)); + + const savedPayload = JSON.parse(fs.readFileSync(payloadFile, "utf8")); + t.deepEqual(savedPayload, mockData.payload); + }); + }, + ); } - test(`uploadPayload on ${analysis.name} wraps request errors using wrapApiConfigurationError`, async (t) => { - const { upload, requestStub } = uploadPayloadFixtures(analysis); - const wrapApiConfigurationErrorStub = sinon.stub( - api, - "wrapApiConfigurationError", - ); - const originalError = new HTTPError(404); - const wrappedError = new Error("Wrapped error message"); - requestStub.rejects(originalError); - wrapApiConfigurationErrorStub.withArgs(originalError).returns(wrappedError); - await t.throwsAsync(upload, { - is: wrappedError, - }); - }); + test.serial( + `uploadPayload on ${analysis.name} wraps request errors using wrapApiConfigurationError`, + async (t) => { + const { upload, requestStub } = uploadPayloadFixtures(analysis); + const wrapApiConfigurationErrorStub = sinon.stub( + api, + "wrapApiConfigurationError", + ); + const originalError = new HTTPError(404); + const wrappedError = new Error("Wrapped error message"); + requestStub.rejects(originalError); + wrapApiConfigurationErrorStub + .withArgs(originalError) + .returns(wrappedError); + await t.throwsAsync(upload, { + is: wrappedError, + }); + }, + ); } diff --git a/src/upload-sarif.test.ts b/src/upload-sarif.test.ts index e7ee91174d..fcd5c3108f 100644 --- a/src/upload-sarif.test.ts +++ b/src/upload-sarif.test.ts @@ -123,7 +123,7 @@ const postProcessAndUploadSarifMacro = test.macro({ title: (providedTitle = "") => `processAndUploadSarif - ${providedTitle}`, }); -test( +test.serial( "SARIF file", postProcessAndUploadSarifMacro, ["test.sarif"], @@ -138,7 +138,7 @@ test( }, ); -test( +test.serial( "JSON file", postProcessAndUploadSarifMacro, ["test.json"], @@ -153,7 +153,7 @@ test( }, ); -test( +test.serial( "Code Scanning files", postProcessAndUploadSarifMacro, ["test.json", "test.sarif"], @@ -169,7 +169,7 @@ test( }, ); -test( +test.serial( "Code Quality file", postProcessAndUploadSarifMacro, ["test.quality.sarif"], @@ -184,7 +184,7 @@ test( }, ); -test( +test.serial( "Mixed files", postProcessAndUploadSarifMacro, ["test.sarif", "test.quality.sarif"], @@ -207,64 +207,70 @@ test( }, ); -test("postProcessAndUploadSarif doesn't upload if upload is disabled", async (t) => { - await util.withTmpDir(async (tempDir) => { - const logger = getRunnerLogger(true); - const features = createFeatures([]); +test.serial( + "postProcessAndUploadSarif doesn't upload if upload is disabled", + async (t) => { + await util.withTmpDir(async (tempDir) => { + const logger = getRunnerLogger(true); + const features = createFeatures([]); - const toFullPath = (filename: string) => path.join(tempDir, filename); + const toFullPath = (filename: string) => path.join(tempDir, filename); - const postProcessSarifFiles = mockPostProcessSarifFiles(); - const uploadPostProcessedFiles = sinon.stub( - uploadLib, - "uploadPostProcessedFiles", - ); + const postProcessSarifFiles = mockPostProcessSarifFiles(); + const uploadPostProcessedFiles = sinon.stub( + uploadLib, + "uploadPostProcessedFiles", + ); - fs.writeFileSync(toFullPath("test.sarif"), ""); - fs.writeFileSync(toFullPath("test.quality.sarif"), ""); + fs.writeFileSync(toFullPath("test.sarif"), ""); + fs.writeFileSync(toFullPath("test.quality.sarif"), ""); - const actual = await postProcessAndUploadSarif( - logger, - features, - "never", - "", - tempDir, - ); + const actual = await postProcessAndUploadSarif( + logger, + features, + "never", + "", + tempDir, + ); - t.truthy(actual); - t.assert(postProcessSarifFiles.calledTwice); - t.assert(uploadPostProcessedFiles.notCalled); - }); -}); + t.truthy(actual); + t.assert(postProcessSarifFiles.calledTwice); + t.assert(uploadPostProcessedFiles.notCalled); + }); + }, +); -test("postProcessAndUploadSarif writes post-processed SARIF files if output directory is provided", async (t) => { - await util.withTmpDir(async (tempDir) => { - const logger = getRunnerLogger(true); - const features = createFeatures([]); +test.serial( + "postProcessAndUploadSarif writes post-processed SARIF files if output directory is provided", + async (t) => { + await util.withTmpDir(async (tempDir) => { + const logger = getRunnerLogger(true); + const features = createFeatures([]); - const toFullPath = (filename: string) => path.join(tempDir, filename); + const toFullPath = (filename: string) => path.join(tempDir, filename); - const postProcessSarifFiles = mockPostProcessSarifFiles(); + const postProcessSarifFiles = mockPostProcessSarifFiles(); - fs.writeFileSync(toFullPath("test.sarif"), ""); - fs.writeFileSync(toFullPath("test.quality.sarif"), ""); + fs.writeFileSync(toFullPath("test.sarif"), ""); + fs.writeFileSync(toFullPath("test.quality.sarif"), ""); - const postProcessedOutPath = path.join(tempDir, "post-processed"); - const actual = await postProcessAndUploadSarif( - logger, - features, - "never", - "", - tempDir, - "", - postProcessedOutPath, - ); + const postProcessedOutPath = path.join(tempDir, "post-processed"); + const actual = await postProcessAndUploadSarif( + logger, + features, + "never", + "", + tempDir, + "", + postProcessedOutPath, + ); - t.truthy(actual); - t.assert(postProcessSarifFiles.calledTwice); - t.assert(fs.existsSync(path.join(postProcessedOutPath, "upload.sarif"))); - t.assert( - fs.existsSync(path.join(postProcessedOutPath, "upload.quality.sarif")), - ); - }); -}); + t.truthy(actual); + t.assert(postProcessSarifFiles.calledTwice); + t.assert(fs.existsSync(path.join(postProcessedOutPath, "upload.sarif"))); + t.assert( + fs.existsSync(path.join(postProcessedOutPath, "upload.quality.sarif")), + ); + }); + }, +); diff --git a/src/util.test.ts b/src/util.test.ts index a7e49d470b..63b9263e0d 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -68,22 +68,25 @@ for (const { expectedMemoryValue, reservedPercentageValue, } of GET_MEMORY_FLAG_TESTS) { - test(`Memory flag value is ${expectedMemoryValue} for ${ - input ?? "no user input" - } on ${platform} with ${totalMemoryMb} MB total system RAM${ - reservedPercentageValue - ? ` and reserved percentage env var set to ${reservedPercentageValue}` - : "" - }`, async (t) => { - process.env[EnvVar.SCALING_RESERVED_RAM_PERCENTAGE] = - reservedPercentageValue || undefined; - const flag = util.getMemoryFlagValueForPlatform( - input, - totalMemoryMb * 1024 * 1024, - platform, - ); - t.deepEqual(flag, expectedMemoryValue); - }); + test.serial( + `Memory flag value is ${expectedMemoryValue} for ${ + input ?? "no user input" + } on ${platform} with ${totalMemoryMb} MB total system RAM${ + reservedPercentageValue + ? ` and reserved percentage env var set to ${reservedPercentageValue}` + : "" + }`, + async (t) => { + process.env[EnvVar.SCALING_RESERVED_RAM_PERCENTAGE] = + reservedPercentageValue || undefined; + const flag = util.getMemoryFlagValueForPlatform( + input, + totalMemoryMb * 1024 * 1024, + platform, + ); + t.deepEqual(flag, expectedMemoryValue); + }, + ); } test("getMemoryFlag() throws if the ram input is < 0 or NaN", async (t) => { @@ -114,19 +117,22 @@ test("getThreadsFlag() throws if the threads input is not an integer", (t) => { t.throws(() => util.getThreadsFlag("hello!", getRunnerLogger(true))); }); -test("getExtraOptionsEnvParam() succeeds on valid JSON with invalid options (for now)", (t) => { - const origExtraOptions = process.env.CODEQL_ACTION_EXTRA_OPTIONS; +test.serial( + "getExtraOptionsEnvParam() succeeds on valid JSON with invalid options (for now)", + (t) => { + const origExtraOptions = process.env.CODEQL_ACTION_EXTRA_OPTIONS; - const options = { foo: 42 }; + const options = { foo: 42 }; - process.env.CODEQL_ACTION_EXTRA_OPTIONS = JSON.stringify(options); + process.env.CODEQL_ACTION_EXTRA_OPTIONS = JSON.stringify(options); - t.deepEqual(util.getExtraOptionsEnvParam(), options); + t.deepEqual(util.getExtraOptionsEnvParam(), options); - process.env.CODEQL_ACTION_EXTRA_OPTIONS = origExtraOptions; -}); + process.env.CODEQL_ACTION_EXTRA_OPTIONS = origExtraOptions; + }, +); -test("getExtraOptionsEnvParam() succeeds on valid JSON options", (t) => { +test.serial("getExtraOptionsEnvParam() succeeds on valid JSON options", (t) => { const origExtraOptions = process.env.CODEQL_ACTION_EXTRA_OPTIONS; const options = { database: { init: ["--debug"] } }; @@ -137,7 +143,7 @@ test("getExtraOptionsEnvParam() succeeds on valid JSON options", (t) => { process.env.CODEQL_ACTION_EXTRA_OPTIONS = origExtraOptions; }); -test("getExtraOptionsEnvParam() succeeds on valid YAML options", (t) => { +test.serial("getExtraOptionsEnvParam() succeeds on valid YAML options", (t) => { const origExtraOptions = process.env.CODEQL_ACTION_EXTRA_OPTIONS; const options = { database: { init: ["--debug"] } }; @@ -148,7 +154,7 @@ test("getExtraOptionsEnvParam() succeeds on valid YAML options", (t) => { process.env.CODEQL_ACTION_EXTRA_OPTIONS = origExtraOptions; }); -test("getExtraOptionsEnvParam() fails on invalid JSON", (t) => { +test.serial("getExtraOptionsEnvParam() fails on invalid JSON", (t) => { const origExtraOptions = process.env.CODEQL_ACTION_EXTRA_OPTIONS; process.env.CODEQL_ACTION_EXTRA_OPTIONS = "{{invalid-json}"; @@ -233,7 +239,7 @@ test("allowed API versions", async (t) => { ); }); -test("getRequiredEnvParam - gets environment variables", (t) => { +test.serial("getRequiredEnvParam - gets environment variables", (t) => { process.env.SOME_UNIT_TEST_VAR = "foo"; const result = util.getRequiredEnvParam("SOME_UNIT_TEST_VAR"); t.is(result, "foo"); @@ -243,17 +249,20 @@ test("getRequiredEnvParam - throws if an environment variable isn't set", (t) => t.throws(() => util.getRequiredEnvParam("SOME_UNIT_TEST_VAR")); }); -test("getOptionalEnvVar - gets environment variables", (t) => { +test.serial("getOptionalEnvVar - gets environment variables", (t) => { process.env.SOME_UNIT_TEST_VAR = "foo"; const result = util.getOptionalEnvVar("SOME_UNIT_TEST_VAR"); t.is(result, "foo"); }); -test("getOptionalEnvVar - gets undefined for empty environment variables", (t) => { - process.env.SOME_UNIT_TEST_VAR = ""; - const result = util.getOptionalEnvVar("SOME_UNIT_TEST_VAR"); - t.is(result, undefined); -}); +test.serial( + "getOptionalEnvVar - gets undefined for empty environment variables", + (t) => { + process.env.SOME_UNIT_TEST_VAR = ""; + const result = util.getOptionalEnvVar("SOME_UNIT_TEST_VAR"); + t.is(result, undefined); + }, +); test("getOptionalEnvVar - doesn't throw for undefined environment variables", (t) => { t.notThrows(() => { @@ -405,27 +414,32 @@ for (const [ const versionsDescription = `CodeQL Action version ${version} and GitHub version ${formatGitHubVersion( githubVersion, )}`; - test(`checkActionVersion ${reportErrorDescription} for ${versionsDescription}`, async (t) => { - const warningSpy = sinon.spy(core, "warning"); - const versionStub = sinon - .stub(api, "getGitHubVersion") - .resolves(githubVersion); - - // call checkActionVersion twice and assert below that warning is reported only once - util.checkActionVersion(version, await api.getGitHubVersion()); - util.checkActionVersion(version, await api.getGitHubVersion()); - - if (shouldReportError) { - t.true( - warningSpy.calledOnceWithExactly( - sinon.match("CodeQL Action v3 will be deprecated in December 2026."), - ), - ); - } else { - t.false(warningSpy.called); - } - versionStub.restore(); - }); + test.serial( + `checkActionVersion ${reportErrorDescription} for ${versionsDescription}`, + async (t) => { + const warningSpy = sinon.spy(core, "warning"); + const versionStub = sinon + .stub(api, "getGitHubVersion") + .resolves(githubVersion); + + // call checkActionVersion twice and assert below that warning is reported only once + util.checkActionVersion(version, await api.getGitHubVersion()); + util.checkActionVersion(version, await api.getGitHubVersion()); + + if (shouldReportError) { + t.true( + warningSpy.calledOnceWithExactly( + sinon.match( + "CodeQL Action v3 will be deprecated in December 2026.", + ), + ), + ); + } else { + t.false(warningSpy.called); + } + versionStub.restore(); + }, + ); } test("getCgroupCpuCountFromCpus calculates the number of CPUs correctly", async (t) => { @@ -461,14 +475,17 @@ test("getCgroupCpuCountFromCpus returns undefined if the CPU file exists but is }); }); -test("checkDiskUsage succeeds and produces positive numbers", async (t) => { - process.env["GITHUB_WORKSPACE"] = os.tmpdir(); - const diskUsage = await util.checkDiskUsage(getRunnerLogger(true)); - if (t.truthy(diskUsage)) { - t.true(diskUsage.numAvailableBytes > 0); - t.true(diskUsage.numTotalBytes > 0); - } -}); +test.serial( + "checkDiskUsage succeeds and produces positive numbers", + async (t) => { + process.env["GITHUB_WORKSPACE"] = os.tmpdir(); + const diskUsage = await util.checkDiskUsage(getRunnerLogger(true)); + if (t.truthy(diskUsage)) { + t.true(diskUsage.numAvailableBytes > 0); + t.true(diskUsage.numTotalBytes > 0); + } + }, +); test("joinAtMost - behaves like join if limit is <= 0", (t) => { const sep = ", "; diff --git a/src/workflow.test.ts b/src/workflow.test.ts index f05ad54851..67f9690401 100644 --- a/src/workflow.test.ts +++ b/src/workflow.test.ts @@ -306,7 +306,7 @@ test("getWorkflowErrors() when on.pull_request for wildcard branches", async (t) t.deepEqual(...errorCodes(errors, [])); }); -test("getWorkflowErrors() when HEAD^2 is checked out", async (t) => { +test.serial("getWorkflowErrors() when HEAD^2 is checked out", async (t) => { process.env.GITHUB_JOB = "test"; const errors = await getWorkflowErrors( @@ -320,47 +320,59 @@ test("getWorkflowErrors() when HEAD^2 is checked out", async (t) => { t.deepEqual(...errorCodes(errors, [WorkflowErrors.CheckoutWrongHead])); }); -test("getWorkflowErrors() produces an error for workflow with language name and its alias", async (t) => { - await testLanguageAliases( - t, - ["java", "kotlin"], - { java: ["java-kotlin", "kotlin"] }, - [ - "CodeQL language 'java' is referenced by more than one entry in the 'language' matrix " + - "parameter for job 'test'. This may result in duplicate alerts. Please edit the 'language' " + - "matrix parameter to keep only one of the following: 'java', 'kotlin'.", - ], - ); -}); - -test("getWorkflowErrors() produces an error for workflow with two aliases same language", async (t) => { - await testLanguageAliases( - t, - ["java-kotlin", "kotlin"], - { java: ["java-kotlin", "kotlin"] }, - [ - "CodeQL language 'java' is referenced by more than one entry in the 'language' matrix " + - "parameter for job 'test'. This may result in duplicate alerts. Please edit the 'language' " + - "matrix parameter to keep only one of the following: 'java-kotlin', 'kotlin'.", - ], - ); -}); - -test("getWorkflowErrors() does not produce an error for workflow with two distinct languages", async (t) => { - await testLanguageAliases( - t, - ["java", "typescript"], - { - java: ["java-kotlin", "kotlin"], - javascript: ["javascript-typescript", "typescript"], - }, - [], - ); -}); +test.serial( + "getWorkflowErrors() produces an error for workflow with language name and its alias", + async (t) => { + await testLanguageAliases( + t, + ["java", "kotlin"], + { java: ["java-kotlin", "kotlin"] }, + [ + "CodeQL language 'java' is referenced by more than one entry in the 'language' matrix " + + "parameter for job 'test'. This may result in duplicate alerts. Please edit the 'language' " + + "matrix parameter to keep only one of the following: 'java', 'kotlin'.", + ], + ); + }, +); + +test.serial( + "getWorkflowErrors() produces an error for workflow with two aliases same language", + async (t) => { + await testLanguageAliases( + t, + ["java-kotlin", "kotlin"], + { java: ["java-kotlin", "kotlin"] }, + [ + "CodeQL language 'java' is referenced by more than one entry in the 'language' matrix " + + "parameter for job 'test'. This may result in duplicate alerts. Please edit the 'language' " + + "matrix parameter to keep only one of the following: 'java-kotlin', 'kotlin'.", + ], + ); + }, +); + +test.serial( + "getWorkflowErrors() does not produce an error for workflow with two distinct languages", + async (t) => { + await testLanguageAliases( + t, + ["java", "typescript"], + { + java: ["java-kotlin", "kotlin"], + javascript: ["javascript-typescript", "typescript"], + }, + [], + ); + }, +); -test("getWorkflowErrors() does not produce an error if codeql doesn't support language aliases", async (t) => { - await testLanguageAliases(t, ["java-kotlin", "kotlin"], undefined, []); -}); +test.serial( + "getWorkflowErrors() does not produce an error if codeql doesn't support language aliases", + async (t) => { + await testLanguageAliases(t, ["java-kotlin", "kotlin"], undefined, []); + }, +); async function testLanguageAliases( t: ExecutionContext, @@ -483,11 +495,13 @@ test("getWorkflowErrors() when on.push has a trailing comma", async (t) => { t.deepEqual(...errorCodes(errors, [])); }); -test("getWorkflowErrors() should only report the current job's CheckoutWrongHead", async (t) => { - process.env.GITHUB_JOB = "test"; +test.serial( + "getWorkflowErrors() should only report the current job's CheckoutWrongHead", + async (t) => { + process.env.GITHUB_JOB = "test"; - const errors = await getWorkflowErrors( - yaml.load(` + const errors = await getWorkflowErrors( + yaml.load(` name: "CodeQL" on: push: @@ -507,17 +521,20 @@ test("getWorkflowErrors() should only report the current job's CheckoutWrongHead test3: steps: [] `) as Workflow, - await getCodeQLForTesting(), - ); + await getCodeQLForTesting(), + ); - t.deepEqual(...errorCodes(errors, [WorkflowErrors.CheckoutWrongHead])); -}); + t.deepEqual(...errorCodes(errors, [WorkflowErrors.CheckoutWrongHead])); + }, +); -test("getWorkflowErrors() should not report a different job's CheckoutWrongHead", async (t) => { - process.env.GITHUB_JOB = "test3"; +test.serial( + "getWorkflowErrors() should not report a different job's CheckoutWrongHead", + async (t) => { + process.env.GITHUB_JOB = "test3"; - const errors = await getWorkflowErrors( - yaml.load(` + const errors = await getWorkflowErrors( + yaml.load(` name: "CodeQL" on: push: @@ -537,11 +554,12 @@ test("getWorkflowErrors() should not report a different job's CheckoutWrongHead" test3: steps: [] `) as Workflow, - await getCodeQLForTesting(), - ); + await getCodeQLForTesting(), + ); - t.deepEqual(...errorCodes(errors, [])); -}); + t.deepEqual(...errorCodes(errors, [])); + }, +); test("getWorkflowErrors() when on is missing", async (t) => { const errors = await getWorkflowErrors( @@ -723,11 +741,13 @@ test("getWorkflowErrors() should not report a warning involving versions of othe t.deepEqual(...errorCodes(errors, [])); }); -test("getCategoryInputOrThrow returns category for simple workflow with category", (t) => { - process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; - t.is( - getCategoryInputOrThrow( - yaml.load(` +test.serial( + "getCategoryInputOrThrow returns category for simple workflow with category", + (t) => { + process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; + t.is( + getCategoryInputOrThrow( + yaml.load(` jobs: analysis: runs-on: ubuntu-latest @@ -738,18 +758,21 @@ test("getCategoryInputOrThrow returns category for simple workflow with category with: category: some-category `) as Workflow, - "analysis", - {}, - ), - "some-category", - ); -}); - -test("getCategoryInputOrThrow returns undefined for simple workflow without category", (t) => { - process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; - t.is( - getCategoryInputOrThrow( - yaml.load(` + "analysis", + {}, + ), + "some-category", + ); + }, +); + +test.serial( + "getCategoryInputOrThrow returns undefined for simple workflow without category", + (t) => { + process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; + t.is( + getCategoryInputOrThrow( + yaml.load(` jobs: analysis: runs-on: ubuntu-latest @@ -758,18 +781,21 @@ test("getCategoryInputOrThrow returns undefined for simple workflow without cate - uses: github/codeql-action/init@v4 - uses: github/codeql-action/analyze@v4 `) as Workflow, - "analysis", - {}, - ), - undefined, - ); -}); - -test("getCategoryInputOrThrow returns category for workflow with multiple jobs", (t) => { - process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; - t.is( - getCategoryInputOrThrow( - yaml.load(` + "analysis", + {}, + ), + undefined, + ); + }, +); + +test.serial( + "getCategoryInputOrThrow returns category for workflow with multiple jobs", + (t) => { + process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; + t.is( + getCategoryInputOrThrow( + yaml.load(` jobs: foo: runs-on: ubuntu-latest @@ -790,18 +816,21 @@ test("getCategoryInputOrThrow returns category for workflow with multiple jobs", with: category: bar-category `) as Workflow, - "bar", - {}, - ), - "bar-category", - ); -}); - -test("getCategoryInputOrThrow finds category for workflow with language matrix", (t) => { - process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; - t.is( - getCategoryInputOrThrow( - yaml.load(` + "bar", + {}, + ), + "bar-category", + ); + }, +); + +test.serial( + "getCategoryInputOrThrow finds category for workflow with language matrix", + (t) => { + process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; + t.is( + getCategoryInputOrThrow( + yaml.load(` jobs: analysis: runs-on: ubuntu-latest @@ -817,19 +846,22 @@ test("getCategoryInputOrThrow finds category for workflow with language matrix", with: category: "/language:\${{ matrix.language }}" `) as Workflow, - "analysis", - { language: "javascript" }, - ), - "/language:javascript", - ); -}); - -test("getCategoryInputOrThrow throws error for workflow with dynamic category", (t) => { - process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; - t.throws( - () => - getCategoryInputOrThrow( - yaml.load(` + "analysis", + { language: "javascript" }, + ), + "/language:javascript", + ); + }, +); + +test.serial( + "getCategoryInputOrThrow throws error for workflow with dynamic category", + (t) => { + process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; + t.throws( + () => + getCategoryInputOrThrow( + yaml.load(` jobs: analysis: steps: @@ -839,23 +871,26 @@ test("getCategoryInputOrThrow throws error for workflow with dynamic category", with: category: "\${{ github.workflow }}" `) as Workflow, - "analysis", - {}, - ), - { - message: - "Could not get category input to github/codeql-action/analyze since it contained " + - "an unrecognized dynamic value.", - }, - ); -}); - -test("getCategoryInputOrThrow throws error for workflow with multiple calls to analyze", (t) => { - process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; - t.throws( - () => - getCategoryInputOrThrow( - yaml.load(` + "analysis", + {}, + ), + { + message: + "Could not get category input to github/codeql-action/analyze since it contained " + + "an unrecognized dynamic value.", + }, + ); + }, +); + +test.serial( + "getCategoryInputOrThrow throws error for workflow with multiple calls to analyze", + (t) => { + process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository"; + t.throws( + () => + getCategoryInputOrThrow( + yaml.load(` jobs: analysis: runs-on: ubuntu-latest @@ -869,88 +904,101 @@ test("getCategoryInputOrThrow throws error for workflow with multiple calls to a with: category: another-category `) as Workflow, - "analysis", - {}, - ), - { - message: - "Could not get category input to github/codeql-action/analyze since the analysis job " + - "calls github/codeql-action/analyze multiple times.", - }, - ); -}); - -test("checkWorkflow - validates workflow if `SKIP_WORKFLOW_VALIDATION` is not set", async (t) => { - const messages: LoggedMessage[] = []; - const codeql = createStubCodeQL({}); - - sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false); - const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); - validateWorkflow.resolves(undefined); - - await checkWorkflow(getRecordingLogger(messages), codeql); - - t.assert( - validateWorkflow.calledOnce, - "`checkWorkflow` unexpectedly did not call `validateWorkflow`", - ); - checkExpectedLogMessages(t, messages, [ - "Detected no issues with the code scanning workflow.", - ]); -}); - -test("checkWorkflow - logs problems with workflow validation", async (t) => { - const messages: LoggedMessage[] = []; - const codeql = createStubCodeQL({}); - - sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false); - const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); - validateWorkflow.resolves("problem"); - - await checkWorkflow(getRecordingLogger(messages), codeql); - - t.assert( - validateWorkflow.calledOnce, - "`checkWorkflow` unexpectedly did not call `validateWorkflow`", - ); - checkExpectedLogMessages(t, messages, [ - "Unable to validate code scanning workflow: problem", - ]); -}); - -test("checkWorkflow - skips validation if `SKIP_WORKFLOW_VALIDATION` is `true`", async (t) => { - process.env[EnvVar.SKIP_WORKFLOW_VALIDATION] = "true"; - - const messages: LoggedMessage[] = []; - const codeql = createStubCodeQL({}); - - sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false); - const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); - - await checkWorkflow(getRecordingLogger(messages), codeql); - - t.assert( - validateWorkflow.notCalled, - "`checkWorkflow` called `validateWorkflow` unexpectedly", - ); - t.is(messages.length, 0); -}); - -test("checkWorkflow - skips validation for `dynamic` workflows", async (t) => { - const messages: LoggedMessage[] = []; - const codeql = createStubCodeQL({}); - - const isDynamicWorkflow = sinon - .stub(actionsUtil, "isDynamicWorkflow") - .returns(true); - const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); - - await checkWorkflow(getRecordingLogger(messages), codeql); - - t.assert(isDynamicWorkflow.calledOnce); - t.assert( - validateWorkflow.notCalled, - "`checkWorkflow` called `validateWorkflow` unexpectedly", - ); - t.is(messages.length, 0); -}); + "analysis", + {}, + ), + { + message: + "Could not get category input to github/codeql-action/analyze since the analysis job " + + "calls github/codeql-action/analyze multiple times.", + }, + ); + }, +); + +test.serial( + "checkWorkflow - validates workflow if `SKIP_WORKFLOW_VALIDATION` is not set", + async (t) => { + const messages: LoggedMessage[] = []; + const codeql = createStubCodeQL({}); + + sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false); + const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); + validateWorkflow.resolves(undefined); + + await checkWorkflow(getRecordingLogger(messages), codeql); + + t.assert( + validateWorkflow.calledOnce, + "`checkWorkflow` unexpectedly did not call `validateWorkflow`", + ); + checkExpectedLogMessages(t, messages, [ + "Detected no issues with the code scanning workflow.", + ]); + }, +); + +test.serial( + "checkWorkflow - logs problems with workflow validation", + async (t) => { + const messages: LoggedMessage[] = []; + const codeql = createStubCodeQL({}); + + sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false); + const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); + validateWorkflow.resolves("problem"); + + await checkWorkflow(getRecordingLogger(messages), codeql); + + t.assert( + validateWorkflow.calledOnce, + "`checkWorkflow` unexpectedly did not call `validateWorkflow`", + ); + checkExpectedLogMessages(t, messages, [ + "Unable to validate code scanning workflow: problem", + ]); + }, +); + +test.serial( + "checkWorkflow - skips validation if `SKIP_WORKFLOW_VALIDATION` is `true`", + async (t) => { + process.env[EnvVar.SKIP_WORKFLOW_VALIDATION] = "true"; + + const messages: LoggedMessage[] = []; + const codeql = createStubCodeQL({}); + + sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false); + const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); + + await checkWorkflow(getRecordingLogger(messages), codeql); + + t.assert( + validateWorkflow.notCalled, + "`checkWorkflow` called `validateWorkflow` unexpectedly", + ); + t.is(messages.length, 0); + }, +); + +test.serial( + "checkWorkflow - skips validation for `dynamic` workflows", + async (t) => { + const messages: LoggedMessage[] = []; + const codeql = createStubCodeQL({}); + + const isDynamicWorkflow = sinon + .stub(actionsUtil, "isDynamicWorkflow") + .returns(true); + const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow"); + + await checkWorkflow(getRecordingLogger(messages), codeql); + + t.assert(isDynamicWorkflow.calledOnce); + t.assert( + validateWorkflow.notCalled, + "`checkWorkflow` called `validateWorkflow` unexpectedly", + ); + t.is(messages.length, 0); + }, +);