Run some unit tests in parallel

This commit is contained in:
Henry Mercer
2026-03-04 12:38:18 +01:00
parent 281b265245
commit 675af55c60
39 changed files with 4647 additions and 4029 deletions

View File

@@ -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"

2
lib/analyze-action.js generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

2
lib/init-action.js generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

2
lib/upload-lib.js generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -133,75 +133,87 @@ test("getPullRequestBranches() returns undefined with push context", (t) => {
);
});
test("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.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 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_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("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 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("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 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 +261,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());

View File

@@ -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.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("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 - 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 - 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;
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);
});
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.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("Risk Assessment configuration throws for invalid IDs", (t) => {
test.serial("Risk Assessment configuration throws for invalid IDs", (t) => {
process.env[EnvVar.RISK_ASSESSMENT_ID] = "foo";
t.throws(
() =>

View File

@@ -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);

View File

@@ -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);
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);
// 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);
// 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"));
// 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(
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(
"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 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 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 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"),
);
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(),
];
// 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),
),
);
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),
),
);
}
}
}
});
},
);

View File

@@ -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);
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);
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}`));
});
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) => {

View File

@@ -120,19 +120,53 @@ async function stubCodeql(): Promise<codeql.CodeQL> {
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.serial(
"downloads an explicitly requested bundle even if a different version is cached",
async (t) => {
const features = createFeatures([]);
test("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 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 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 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,
test.serial(
`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 installIntoToolcache({
tagName: "codeql-bundle-20200601",
isPinned: false,
tmpDir,
});
mockBundleDownloadApi({
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 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, 1);
});
});
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 installIntoToolcache({
tagName: "codeql-bundle-20200601",
isPinned: false,
tmpDir,
const cachedVersions = toolcache.findAllVersions("CodeQL");
t.is(cachedVersions.length, 2);
});
},
);
mockBundleDownloadApi({
tagName: defaults.bundleVersion,
});
const result = await codeql.setupCodeQL(
undefined,
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
util.GitHubVariant.GHES,
{
cliVersion: defaults.cliVersion,
test.serial(
'downloads bundle if "latest" tools specified but not cached',
async (t) => {
const features = createFeatures([]);
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
await installIntoToolcache({
tagName: "codeql-bundle-20200601",
isPinned: true,
tmpDir,
});
mockBundleDownloadApi({
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 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('downloads bundle if "latest" tools specified but not cached', async (t) => {
const features = createFeatures([]);
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
await installIntoToolcache({
tagName: "codeql-bundle-20200601",
isPinned: true,
tmpDir,
const cachedVersions = toolcache.findAllVersions("CodeQL");
t.is(cachedVersions.length, 2);
});
},
);
mockBundleDownloadApi({
tagName: defaults.bundleVersion,
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);
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);
}
const cachedVersions = toolcache.findAllVersions("CodeQL");
t.is(cachedVersions.length, 1);
t.is(cachedVersions[0], "0.0.0-20230203");
t.false(releasesApiMock.isDone());
});
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([]);
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,
);
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");
t.false(releasesApiMock.isDone());
});
});
},
);
function assertDurationsInteger(
t: ExecutionContext<unknown>,
@@ -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<unknown>) => {
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<unknown>) => {
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<unknown>) => {
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<unknown>) => {
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("");
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\\.",
),
},
);
});
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,

View File

@@ -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";
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";
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
},
};
},
const codeql = createStubCodeQL({
async betterResolveLanguages() {
return {
extractors: {
javascript: [{ extractor_root: "" }],
},
};
},
});
// This should be ignored and no error should be thrown.
const repositoryProperties = {
"github-codeql-extra-queries": "+foo",
};
// Expected configuration for a CQ-only analysis.
const computedConfig: UserConfig = {
"disable-default-queries": true,
queries: [{ uses: "code-quality" }],
"query-filters": [],
};
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);
});
});
},
);
// This should be ignored and no error should be thrown.
const repositoryProperties = {
"github-codeql-extra-queries": "+foo",
};
// Expected configuration for a CQ-only analysis.
const computedConfig: UserConfig = {
"disable-default-queries": true,
queries: [{ uses: "code-quality" }],
"query-filters": [],
};
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);
});
});
});
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";
const config = await configUtils.initConfig(
createFeatures([]),
createTestInitConfigInputs({
languagesInput,
configFile: configFilePath,
configInput,
tempDir,
codeql,
workspacePath: tempDir,
}),
);
t.deepEqual(config.originalUserInput, yaml.load(configInput));
});
},
);
// 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,
}),
);
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],

View File

@@ -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);
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,
);
});
});
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);
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);
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,
);
});
});
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 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);
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,
);
});
});
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("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

View File

@@ -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([]);
test.serial(
"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()));
});
});
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]);
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()));
});
});
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");
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();
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);
});
});
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");
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);
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);
});
});
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");
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);
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);
});
});
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 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 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 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("downloadDependencyCaches - restores caches with feature keys if features are enabled", async (t) => {
process.env["RUNNER_OS"] = "Linux";
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 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 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");
// 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 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 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 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,62 @@ 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],
});
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 makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub.resolves(undefined);
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);
});
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";
test.serial(
"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 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);
const primaryCacheKey = await cacheKey(
codeql,
features,
KnownLanguage.csharp,
CSHARP_BASE_PATTERNS,
);
const primaryCacheKey = await cacheKey(
codeql,
features,
KnownLanguage.csharp,
CSHARP_BASE_PATTERNS,
);
const config = createTestConfig({
languages: [KnownLanguage.csharp],
dependencyCachingRestoredKeys: [primaryCacheKey],
});
const config = createTestConfig({
languages: [KnownLanguage.csharp],
dependencyCachingRestoredKeys: [primaryCacheKey],
});
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 - 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 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(0);
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.Empty);
checkExpectedLogMessages(t, messages, [
"Skipping upload of dependency cache",
]);
});
test("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 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").resolves();
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);
checkExpectedLogMessages(t, messages, ["Uploading cache of size"]);
});
test("uploadDependencyCaches - catches `ReserveCacheError` exceptions", 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);
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,
@@ -560,12 +501,133 @@ 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);
},
);
checkExpectedLogMessages(t, messages, ["Not uploading cache for"]);
});
});
test.serial(
"uploadDependencyCaches - skips upload if cache size is 0",
async (t) => {
process.env["RUNNER_OS"] = "Linux";
test("uploadDependencyCaches - throws other exceptions", async (t) => {
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);
sinon.stub(cachingUtils, "getTotalCacheSize").resolves(0);
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.Empty);
checkExpectedLogMessages(t, messages, [
"Skipping upload of dependency cache",
]);
},
);
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 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").resolves();
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);
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([]);
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({});

View File

@@ -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);
});

View File

@@ -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,
);
}
assertAllFeaturesUndefinedInApi(t, loggedMessages);
});
});
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: {},
};
});
// 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(
"API response missing and features use default value",
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(403, {});
for (const feature of Object.values(Feature)) {
t.assert(
(await getFeatureIncludingCodeQlIfRequired(features, feature)) ===
featureConfig[feature].defaultValue,
);
}
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
assertAllFeaturesUndefinedInApi(t, loggedMessages);
});
},
);
// 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,
test.serial(
"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,
);
}
// 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);
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: {},
};
});
// feature should be disabled initially
t.assert(
!(await getFeatureIncludingCodeQlIfRequired(
features,
feature as Feature,
)),
);
// 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",
},
);
});
});
},
);
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);
// 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);
// 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);
});
},
);
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(false);
mockFeatureFlagApiEndpoint(200, expectedFeatureEnablement);
// feature should be disabled initially
t.assert(
!(await getFeatureIncludingCodeQlIfRequired(
features,
feature as Feature,
)),
);
// 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,
),
);
});
},
);
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 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 getFeatureIncludingCodeQlIfRequired(
features,
feature as Feature,
)),
);
});
},
);
if (
featureConfig[feature].minimumVersion !== undefined ||
featureConfig[feature].toolsFeature !== undefined
) {
test(`Getting feature '${feature} should throw if no codeql is provided`, async (t) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
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);
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.`,
},
);
});
});
// 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(`Feature '${feature}' is disabled if the minimum CLI version is below ${featureConfig[feature].minimumVersion}`, async (t) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
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);
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)));
// 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)));
// 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));
// 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)));
});
});
// 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) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
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);
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)));
// 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)));
// 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,
// 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)));
});
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);
});
},
);

View File

@@ -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/,
},
);
},
);
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,
),
{
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 properties API to return an array/,
},
);
});
);
t.deepEqual(response, {});
},
);
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(
{
type: util.GitHubVariant.DOTCOM,
},
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, {});
});
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.",
);
},
);

View File

@@ -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;
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 callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs("HEAD").resolves(currentSha);
const actualRef = await gitUtils.getRef();
t.deepEqual(actualRef, expectedRef);
callback.restore();
});
});
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);
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 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();
});
});
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);
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 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();
});
});
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));
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);
// 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 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();
});
});
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;
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);
});
});
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;
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);
});
});
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");
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();
});
});
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));
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();
});
});
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",
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",
);
await t.throwsAsync(
async () => {
await gitUtils.getFileOidsUnderPath("/fake/path");
},
{
instanceOf: Error,
message: 'Unexpected "git ls-files" output: invalid-line-format',
},
);
},
);
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}`);
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");
},
);
const version = await gitUtils.getGitVersionOrThrow();
t.is(version.truncatedVersion, "2.40.0");
t.is(version.fullVersion, "2.40.0");
});
test("getGitVersionOrThrow throws for invalid git output", async (t) => {
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");
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");
});
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 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

View File

@@ -26,7 +26,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) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = tmpDir;
@@ -60,7 +60,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) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = 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,221 +259,202 @@ 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 }}",
},
},
]);
await testFailedSarifUpload(t, actionsWorkflow, {
category: "/language:csharp",
matrix: { language: "csharp" },
});
},
);
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",
},
{
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' }}",
},
},
]);
const result = await testFailedSarifUpload(t, actionsWorkflow, {
category: "my-category",
expectUpload: shouldUpload,
expectUpload: false,
});
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 }}",
},
},
]);
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: "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) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["GITHUB_RUN_ID"] = "12345";
process.env["GITHUB_RUN_ATTEMPT"] = "1";
process.env["GITHUB_JOB"] = "analyze";
process.env["RUNNER_TEMP"] = 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.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: 12345,
workflowRunAttempt: 1,
name: "analyze",
},
name: "Checkout repository",
uses: "actions/checkout@v5",
},
"fourth arg should be the overlay status recording an unsuccessful build attempt with job details",
);
});
});
test("does not save overlay status when OverlayAnalysisStatusSave feature flag is disabled", async (t) => {
return await util.withTmpDir(async (tmpDir) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = 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 result = await testFailedSarifUpload(t, actionsWorkflow, {
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_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);
},
);
t.true(
saveOverlayStatusStub.notCalled,
"saveOverlayStatus should not be called when OverlayAnalysisStatusSave feature flag is disabled",
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,
});
t.is(
result.upload_failed_run_skipped_because,
"Code Scanning is not enabled.",
);
});
});
},
);
test("does not save overlay status when build successful", async (t) => {
test.serial(
"saves overlay status when overlay-base analysis did not complete successfully",
async (t) => {
return await util.withTmpDir(async (tmpDir) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["GITHUB_RUN_ID"] = "12345";
process.env["GITHUB_RUN_ATTEMPT"] = "1";
process.env["GITHUB_JOB"] = "analyze";
process.env["RUNNER_TEMP"] = 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.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: 12345,
workflowRunAttempt: 1,
name: "analyze",
},
},
"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) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = 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.serial("does not save overlay status when build successful", async (t) => {
return await util.withTmpDir(async (tmpDir) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = tmpDir;
@@ -459,41 +491,44 @@ 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) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = 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) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["RUNNER_TEMP"] = 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,
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),
);
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[],

View File

@@ -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;
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 dbLocation = path.resolve(tmpDir, "dbs");
fs.mkdirSync(dbLocation, { recursive: true });
const fileToCleanUp = path.resolve(
dbLocation,
"something-to-cleanup.txt",
);
fs.writeFileSync(fileToCleanUp, "");
const fileToCleanUp = path.resolve(
dbLocation,
"something-to-cleanup.txt",
);
fs.writeFileSync(fileToCleanUp, "");
const rmSyncError = `Failed to clean up file ${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}`,
},
);
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);
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([Feature.SkipFileCoverageOnPrs]),
),
);
},
);
test("file coverage information enabled when owner is not 'github'", async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
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]),
),
);
});
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 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.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
),
);
},
);
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 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]),
),
);
});
t.false(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
);
},
);

View File

@@ -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 });
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 });
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);
// 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();
// 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);
// 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();
// 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[] };
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",
);
});
});
t.deepEqual(
parsedContent.changes.sort(),
["added.js", "deleted.js", "modified.js"],
"Should identify added, deleted, and modified files",
);
});
},
);
interface DownloadOverlayBaseDatabaseTestCase {
overlayDatabaseMode: OverlayDatabaseMode;
@@ -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";

View File

@@ -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);
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);
sinon.stub(actionsCache, "restoreCache").resolves(undefined);
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
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";
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,
);
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);
t.true(result);
});
});
const status = {
attemptedToBuildOverlayBaseDatabase: true,
builtOverlayBaseDatabase: false,
};
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);
// 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 status = {
attemptedToBuildOverlayBaseDatabase: true,
builtOverlayBaseDatabase: true,
};
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
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";
t.true(result);
});
},
);
const result = await shouldSkipOverlayAnalysis(
codeql,
["javascript"],
makeDiskUsage(50),
logger,
);
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);
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",
),
),
);
});
});
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",
),
),
);
});
},
);

View File

@@ -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,361 +95,385 @@ test("getCodeQLActionRepository", (t) => {
t.deepEqual(repoEnv, "xxx/yyy");
});
test("getCodeQLSource sets CLI version for a semver tagged bundle", async (t) => {
const features = createFeatures([]);
test.serial(
"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),
);
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),
);
t.is(source.sourceType, "download");
t.is(source["cliVersion"], "1.2.3");
});
});
t.is(source.sourceType, "download");
t.is(source["cliVersion"], "1.2.3");
});
},
);
test("getCodeQLSource correctly returns bundled CLI version when tools == linked", async (t) => {
const features = createFeatures([]);
test.serial(
"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),
);
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.toolsVersion, LINKED_CLI_VERSION.cliVersion);
t.is(source.sourceType, "download");
});
});
t.is(source.toolsVersion, LINKED_CLI_VERSION.cliVersion);
t.is(source.sourceType, "download");
});
},
);
test("getCodeQLSource correctly returns bundled CLI version when tools == latest", async (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const features = createFeatures([]);
test.serial(
"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(
"latest",
SAMPLE_DEFAULT_CLI_VERSION,
SAMPLE_DOTCOM_API_DETAILS,
GitHubVariant.DOTCOM,
false,
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");
// 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),
),
);
});
});
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,
);
// 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),
),
);
});
});
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,
);
// 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),
),
);
});
});
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,
);
// 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("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);
process.env["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("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);
process.env["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) {
// 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(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(
"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,
);
// 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),
),
);
});
},
);
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,
});
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,
);
// 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),
),
);
});
},
);
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);
process.env["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);
process.env["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 (
@@ -511,7 +535,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],
@@ -523,7 +547,7 @@ test(
],
);
test(
test.serial(
"the workflow trigger is not `dynamic`",
toolcacheInputFallbackMacro,
[Feature.AllowToolcacheInput],
@@ -534,7 +558,7 @@ test(
],
);
test(
test.serial(
"the feature flag is not enabled",
toolcacheInputFallbackMacro,
[],
@@ -543,24 +567,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");
},
);

View File

@@ -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" },
]);
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" },
]);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
registrySecrets,
registryCredentials,
undefined,
);
t.is(credentials.length, 1);
t.is(credentials[0].host, "npm.pkg.github.com");
});
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 throws an error when configurations are not an array",
async (t) => {
const registryCredentials = Buffer.from(
JSON.stringify({ type: "npm_registry", token: "abc" }),
).toString("base64");
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
undefined,
registryCredentials,
undefined,
),
{
message:
"Expected credentials data to be an array of configurations, but it is not.",
},
);
});
test("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,
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 have a type",
},
for (const testCredential of testCredentials) {
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
undefined,
testCredential,
undefined,
),
{
message: "Invalid credentials - must be an object",
},
);
}
},
);
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);
test("getCredentials filters by language when specified", async (t) => {
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 filters by language when specified", async (t) => {
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
undefined,
@@ -238,97 +253,113 @@ 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("getCredentials returns all credentials when no language specified", async (t) => {
const credentialsInput = toEncodedJSON(mixedCredentials);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
undefined,
credentialsInput,
undefined,
);
t.is(credentials.length, mixedCredentials.length);
});
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
];
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",
},
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 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 credentialsTypes = credentials.map((c) => c.type);
t.assert(credentialsTypes.includes("goproxy_server"));
t.assert(credentialsTypes.includes("git_source"));
},
);
const results = startProxyExports.getCredentials(
logger,
undefined,
likelyWrongCredentials,
undefined,
);
test.serial(
"getCredentials returns all credentials when no language specified",
async (t) => {
const credentialsInput = toEncodedJSON(mixedCredentials);
// 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_"));
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
undefined,
credentialsInput,
undefined,
);
t.is(credentials.length, mixedCredentials.length);
},
);
// A warning should have been logged.
checkExpectedLogMessages(t, loggedMessages, [
"using a GitHub Personal Access Token (PAT), but no username was provided",
]);
});
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
];
test("parseLanguage", async (t) => {
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",
},
);
}
},
);
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([
{
type: "git_server",
host: "https://github.com/",
password: `ghp_${makeTestToken()}`,
},
]);
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",
]);
},
);
test.serial("parseLanguage", async (t) => {
// Exact matches
t.deepEqual(parseLanguage("csharp"), KnownLanguage.csharp);
t.deepEqual(parseLanguage("cpp"), KnownLanguage.cpp);
@@ -391,34 +422,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();
test.serial(
"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);
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,
@@ -429,13 +440,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" },
@@ -455,7 +492,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",
@@ -472,7 +509,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",
@@ -489,29 +526,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 (
@@ -530,7 +573,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);
@@ -544,7 +587,7 @@ test("downloadProxy - returns file path on success", async (t) => {
});
});
test(
test.serial(
"downloadProxy",
wrapFailureTest,
() => {
@@ -555,7 +598,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);
@@ -565,7 +608,7 @@ test("extractProxy - returns file path on success", async (t) => {
});
});
test(
test.serial(
"extractProxy",
wrapFailureTest,
() => {
@@ -576,7 +619,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);
@@ -591,7 +634,7 @@ test("cacheProxy - returns file path on success", async (t) => {
});
});
test(
test.serial(
"cacheProxy",
wrapFailureTest,
() => {
@@ -602,100 +645,37 @@ test(
},
);
test("getProxyBinaryPath - returns path from tool cache if available", async (t) => {
const logger = new RecordingLogger();
mockGetReleaseByTag();
test.serial(
"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);
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);
const features = mockOfflineFeatures(tempDir, logger);
const path = await startProxyExports.getProxyBinaryPath(logger, features);
t.assert(path);
t.is(
path,
filepath.join(toolcachePath, startProxyExports.getProxyFilename()),
);
});
});
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 },
]);
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 },
]);
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([]),
);
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 '${defaults.bundleVersion}' at '${downloadUrl}'`,
]);
});
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,
},
];
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({
@@ -716,40 +696,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}'`,
]);
},
);

View File

@@ -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)) {
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);
}
},
);
test.serial(
"checkProxyEnvironment - includes Java checks for Java",
async (t) => {
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
await checkProxyEnvironment(logger, language);
stubToolrunner();
await checkProxyEnvironment(logger, KnownLanguage.java);
assertEnvVarLogMessages(t, Object.keys(ProxyEnvVars), messages, false);
}
});
assertEnvVarLogMessages(t, JAVA_PROXY_ENV_VARS, messages, false);
},
);
test("checkProxyEnvironment - includes Java checks for Java", 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, 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);
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);
},
);

View File

@@ -42,7 +42,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);
@@ -92,7 +92,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);
@@ -112,7 +112,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);
@@ -135,7 +135,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);
@@ -239,58 +239,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",
);
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 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 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(
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(),
"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(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(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",
);
});
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 (
@@ -341,7 +344,7 @@ const testCreateInitWithConfigStatusReport = test.macro({
title: (_, title) => `createInitWithConfigStatusReport: ${title}`,
});
test(
test.serial(
testCreateInitWithConfigStatusReport,
"returns a value",
createTestConfig({
@@ -356,7 +359,7 @@ test(
},
);
test(
test.serial(
testCreateInitWithConfigStatusReport,
"includes packs for a single language",
createTestConfig({
@@ -373,7 +376,7 @@ test(
},
);
test(
test.serial(
testCreateInitWithConfigStatusReport,
"includes packs for multiple languages",
createTestConfig({

View File

@@ -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);

View File

@@ -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);
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");
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",
);
});
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.serial(
"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.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("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.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.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("shouldShowCombineSarifFilesDeprecationWarning with only 1 run", async (t) => {
t.false(
await uploadLib.shouldShowCombineSarifFilesDeprecationWarning(
[createMockSarif("abc", "def")],
{
type: GitHubVariant.DOTCOM,
},
),
);
});
test.serial(
"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.serial(
"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.serial(
"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";
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,
},
),
);
});
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("throwIfCombineSarifFilesDisabled with distinct tools", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "abc"), createMockSarif("abc", "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("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.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));
const error2 = [
"rejecting delivery as the repository has too many logical alerts",
];
t.true(uploadLib.shouldConsiderConfigurationError(error2));
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("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.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));
const error2 = [
"locationFromSarifResult: expected artifact location",
"an invalid URI was provided as a SARIF location",
];
t.true(uploadLib.shouldConsiderInvalidRequest(error2));
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));
});
// 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);
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 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);
});
});
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,
});
},
);
}

View File

@@ -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")),
);
});
},
);

View File

@@ -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(), <any>options);
t.deepEqual(util.getExtraOptionsEnvParam(), <any>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);
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());
// 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();
});
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 = ", ";

View File

@@ -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.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("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() 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() 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<unknown>,
@@ -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",
);
});
"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(`
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,
);
});
"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(`
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",
);
});
"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(`
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",
);
});
"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(`
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.",
},
);
});
"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(`
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.",
},
);
});
"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({});
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);
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
validateWorkflow.resolves(undefined);
await checkWorkflow(getRecordingLogger(messages), codeql);
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.",
]);
});
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({});
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");
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
validateWorkflow.resolves("problem");
await checkWorkflow(getRecordingLogger(messages), codeql);
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",
]);
});
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";
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({});
const messages: LoggedMessage[] = [];
const codeql = createStubCodeQL({});
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
await checkWorkflow(getRecordingLogger(messages), codeql);
await checkWorkflow(getRecordingLogger(messages), codeql);
t.assert(
validateWorkflow.notCalled,
"`checkWorkflow` called `validateWorkflow` unexpectedly",
);
t.is(messages.length, 0);
});
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({});
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");
const isDynamicWorkflow = sinon
.stub(actionsUtil, "isDynamicWorkflow")
.returns(true);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
await checkWorkflow(getRecordingLogger(messages), codeql);
await checkWorkflow(getRecordingLogger(messages), codeql);
t.assert(isDynamicWorkflow.calledOnce);
t.assert(
validateWorkflow.notCalled,
"`checkWorkflow` called `validateWorkflow` unexpectedly",
);
t.is(messages.length, 0);
});
t.assert(isDynamicWorkflow.calledOnce);
t.assert(
validateWorkflow.notCalled,
"`checkWorkflow` called `validateWorkflow` unexpectedly",
);
t.is(messages.length, 0);
},
);