diff --git a/.github/workflows/StrykerJS.yml b/.github/workflows/StrykerJS.yml new file mode 100644 index 0000000..e3231bf --- /dev/null +++ b/.github/workflows/StrykerJS.yml @@ -0,0 +1,77 @@ +name: StrykerJS + +on: + pull_request: + branches: ["master"] + +env: + NODE_VERSION: "20.9.0" + INCREMENTAL_PATH: reports/stryker-incremental.json + FILESTOSTRYKE_COUNT: 0 + FILESTOSTRYKE: '' + +jobs: + strykerJS_testing: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: npm install + run: npm i + + - name: Install Temporary Dependencies + run: npm install axios adm-zip @actions/core @actions/github + + - name: Download and Extract Incremental File + run: node .github/workflows/download-artifact.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ARTIFACT_NAME: stryker-incremental + + - name: Determine Files to Stryke + run: node .github/workflows/determine-files.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FILESTOSTRYKE_COUNT: ${{ env.FILESTOSTRYKE_COUNT }} + + - name: Run Stryker + run: | + if [ -f /home/runner/work/lets-learn-angular/lets-learn-angular/.github/workflows/extracted_artifact/stryker-incremental.json ]; then + echo "Incremental artifact found, continuing Stryker incremental run." + if [ "$FILESTOSTRYKE" != '' ]; then + echo "Running Stryker incremental with:" && echo "$FILESTOSTRYKE" | tr ' ' '\n' + npx stryker run --incremental --incrementalFile /home/runner/work/lets-learn-angular/lets-learn-angular/.github/workflows/extracted_artifact/stryker-incremental.json + else + echo "No component files were defined to stryke. Unable to run Stryker incremental." + fi + else + echo "No incremental artifact round, running Stryker as initial incremental run." + if [ "$FILESTOSTRYKE" != '' ]; then + echo "Running Stryker with $FILESTOSTRYKE." + npx stryker run --incremental --incrementalFile /home/runner/work/lets-learn-angular/lets-learn-angular/.github/workflows/extracted_artifact/stryker-incremental.json + else + echo "No component files were defined to stryke. Unable to run Stryker." + fi + fi + echo "INCREMENTAL_PATH=/home/runner/work/lets-learn-angular/lets-learn-angular/.github/workflows/extracted_artifact/stryker-incremental.json" >> $GITHUB_ENV + + - name: Upload Stryker Incremental Report + uses: actions/upload-artifact@v4 + with: + name: stryker-incremental_${{ env.FILESTOSTRYKE_COUNT }} + path: ${{ env.INCREMENTAL_PATH }} + + - name: Upload Stryker Coverage Report + uses: actions/upload-artifact@v4 + with: + name: stryker-coverage + path: reports/mutation/mutation.html \ No newline at end of file diff --git a/.github/workflows/determine-files.js b/.github/workflows/determine-files.js new file mode 100644 index 0000000..45ffe96 --- /dev/null +++ b/.github/workflows/determine-files.js @@ -0,0 +1,52 @@ +const core = require('@actions/core'); +const github = require('@actions/github'); + +async function getAllFiles(octokit, owner, repo, path) { + const { data: items } = await octokit.rest.repos.getContent({ + owner, + repo, + path + }); + + let files = []; + + for (const item of items) { + if (item.type === 'file') { + files.push(item.path); + } else if (item.type === 'dir') { + const subFiles = await getAllFiles(octokit, owner, repo, item.path); + files = files.concat(subFiles); + } + } + + return files; +} + +(async () => { + try { + const { context } = github; + const octokit = github.getOctokit(process.env.GITHUB_TOKEN); + let prevCount = parseInt(process.env.FILESTOSTRYKE_COUNT, 10); + + const allFiles = await getAllFiles(octokit, context.repo.owner, context.repo.repo, 'src/app'); + const allFilesWithTests = allFiles + .filter(filename => filename.endsWith('.spec.ts')) + .map(testFile => testFile.replace('.spec.ts', '.ts')); + console.log("All files with tests: ", allFilesWithTests); + + if (allFilesWithTests.length === 0) { + console.log('No test files found'); + core.exportVariable('FILESTOSTRYKE', ''); + core.exportVariable('FILESTOSTRYKE_COUNT', 0); + } else if (allFilesWithTests.length > prevCount) { + const filesToStryke = allFilesWithTests.slice(0, prevCount+1); + core.exportVariable('FILESTOSTRYKE', filesToStryke.join(', ')); + core.exportVariable('FILESTOSTRYKE_COUNT', prevCount+1); + } else { + core.exportVariable('FILESTOSTRYKE', allFilesWithTests.join(', ')); + } + + } catch (error) { + core.setFailed(error.message); + } +})(); diff --git a/.github/workflows/download-artifact.js b/.github/workflows/download-artifact.js new file mode 100644 index 0000000..8ff821d --- /dev/null +++ b/.github/workflows/download-artifact.js @@ -0,0 +1,110 @@ +const axios = require('axios'); +const AdmZip = require('adm-zip'); +const core = require('@actions/core'); +const fs = require('fs'); +const path = require('path'); +const { context } = require('@actions/github'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const OWNER = `${context.repo.owner}`; +const REPO = `${context.repo.repo}`; +const ARTIFACT_NAME = process.env.ARTIFACT_NAME; + +async function getWorkflowId() { + console.log("getWorkflowId"); + const url = `https://api.github.com/repos/${OWNER}/${REPO}/actions/workflows`; + const response = await axios.get(url, { + headers: { Authorization: `token ${GITHUB_TOKEN}` } + }); + console.log(`getWorkflowId Response status: ${response.status}`); + const workflow = response.data.workflows.find(workflow => workflow.name === process.env.GITHUB_WORKFLOW); + return workflow ? workflow.id : null; +} + +async function findLatestIncrementalArtifactId(workflowId) { + if (!workflowId) { + console.error(`Invalid workflowId provided (${workflowId}), unable to find latest incremental artifact`); + return null; + } + + console.log(`getWorkflowRuns for ${workflowId}`); + const workflowRunsUrl = `https://api.github.com/repos/${OWNER}/${REPO}/actions/workflows/${workflowId}/runs`; + const response = await axios.get(workflowRunsUrl, { + headers: { Authorization: `token ${GITHUB_TOKEN}` }, + params: { + status: 'success' + } + }); + console.log(`getWorkflowRuns Response status: ${response.status}`); + const runs = response.data.workflow_runs; + + for (let i = 0; i < runs.length; i++) { + const runId = runs[i].id; + console.log(`getArtifacts for run #${i}, runId: ${runId}`); + const artifactsUrl = `https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${runId}/artifacts`; + const response = await axios.get(artifactsUrl, { headers: { Authorization: `token ${GITHUB_TOKEN}` } }); + console.log(`getArtifacts Response status: ${response.status}`); + + const artifact = response.data.artifacts.find(artifact => artifact.name.includes(ARTIFACT_NAME)); + if (artifact) { + let previousCount = artifact.name.split('_')[1]; + core.exportVariable('FILESTOSTRYKE_COUNT', previousCount); + console.log(`artifactId = ${artifact.id}`); + return artifact.id; + } + } +} + +async function downloadArtifact(artifactId) { + if (!artifactId) { + console.error(`Invalid artifactId provided (${artifactId}), unable to download artifact`); + return null; + } + console.log(`Valid artifactId provided, downloadArtifact #${artifactId}`); + const url = `https://api.github.com/repos/${OWNER}/${REPO}/actions/artifacts/${artifactId}/zip`; + const response = await axios.get(url, { + headers: { + Authorization: `token ${GITHUB_TOKEN}` + }, + responseType: 'arraybuffer' + }); + + console.log(`downloadArtifact Response status: ${response.status}`); + const zipPath = path.join(__dirname, `${ARTIFACT_NAME}.zip`); + fs.writeFileSync(zipPath, Buffer.from(response.data)); + console.log(`zipped artifact path: ${zipPath}`); + return zipPath; +} + +function extractArtifact(zipPath) { + if (!zipPath) { + console.error(`Invalid zipPath provided (${zipPath}), unable to extract artifact`); + return null; + } + console.log(`Valid zipPath provided, extractArtifact from: ${zipPath}`); + const zip = new AdmZip(zipPath); + const extractPath = path.join(__dirname, 'extracted_artifact'); + + zip.extractAllTo(extractPath, true); + console.log(`extracted artifact to: ${extractPath}`); + + const artifactFilePath = path.join(extractPath, 'stryker-incremental.json'); + + if (fs.existsSync(artifactFilePath)) { + console.log(`Artifact file found at: ${artifactFilePath}!`); + } else { + console.error(`Artifact file ${artifactFilePath} not found`); + } +} + +(async () => { + try { + const workflowId = await getWorkflowId(); + const artifactId = await findLatestIncrementalArtifactId(workflowId); + const zipPath = await downloadArtifact(artifactId); + extractArtifact(zipPath); + + } catch (error) { + console.error(`Error: ${error.message}`); + } +})(); diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml deleted file mode 100644 index 16a2887..0000000 --- a/.github/workflows/scheduled.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: FleetWebClientScheduled - -on: - schedule: - - cron: '18 23 * * 1' - -env: - ENVIRONMENT_PATH: src/assets/environment.json - DIST_PATH: dist - NODE_VERSION: "20.9.0" - -jobs: - buildClient: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: npm install - run: npm i - - - name: Run Stryker - run: npx stryker run | tee stryker-output.txt - - - name: Upload Stryker Report - uses: actions/upload-artifact@v4 - with: - name: stryker-report - path: reports/mutation/mutation.html diff --git a/src/app/angular-resources/angular-resources.component.spec.ts b/src/app/angular-resources/angular-resources.component.spec.ts index cf489b3..4eb1568 100644 --- a/src/app/angular-resources/angular-resources.component.spec.ts +++ b/src/app/angular-resources/angular-resources.component.spec.ts @@ -20,4 +20,29 @@ describe('AngularResourcesComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe("timesTwoTestChange", () => { + it("should return the input value multiplied by two", () => { + let results = component.timesTwo(2); + + expect(results).toBe(4); + }); + }); + + describe("timesFourTestChange", () => { + it("should return the input value multiplied by four", () => { + let results = component.timesFour(2); + + expect(results).toBe(8); + }); + }); + + describe("timesEightTest", () => { + it("should return the input value multiplied by eight", () => { + let results = component.timesEight(2); + + expect(results).toBe(16); + }); + }); + }); diff --git a/src/app/angular-resources/angular-resources.component.ts b/src/app/angular-resources/angular-resources.component.ts index c97c257..823e4e0 100644 --- a/src/app/angular-resources/angular-resources.component.ts +++ b/src/app/angular-resources/angular-resources.component.ts @@ -10,6 +10,19 @@ export class AngularResourcesComponent implements OnInit { constructor() { } ngOnInit(): void { + + } + + timesTwo(entry: number): number { + return entry * 2; + } + + timesFour(entry: number): number { + return entry * 4; + } + + timesEight(entry: number): number { + return entry * 8; } } diff --git a/src/app/app-footer/app-footer.component.spec.ts b/src/app/app-footer/app-footer.component.spec.ts index f6c0775..13a40ac 100644 --- a/src/app/app-footer/app-footer.component.spec.ts +++ b/src/app/app-footer/app-footer.component.spec.ts @@ -20,4 +20,28 @@ describe('AppFooterComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe("timesTwoTestChange", () => { + it("should return the input value multiplied by two", () => { + let results = component.timesTwo(2); + + expect(results).toBe(4); + }); + }); + + describe("timesFourTestChange", () => { + it("should return the input value multiplied by four", () => { + let results = component.timesFour(2); + + expect(results).toBe(8); + }); + }); + + describe("timesEightTest", () => { + it("should return the input value multiplied by eight", () => { + let results = component.timesEight(2); + + expect(results).toBe(16); + }); + }); }); diff --git a/src/app/app-footer/app-footer.component.ts b/src/app/app-footer/app-footer.component.ts index 4818149..fb3f10d 100644 --- a/src/app/app-footer/app-footer.component.ts +++ b/src/app/app-footer/app-footer.component.ts @@ -15,4 +15,15 @@ export class AppFooterComponent implements OnInit { ngOnInit(): void { } + timesTwo(entry: number): number { + return entry * 2; + } + + timesFour(entry: number): number { + return entry * 4; + } + + timesEight(entry: number): number { + return entry * 8; + } } diff --git a/src/app/app-header/app-header.component.spec.ts b/src/app/app-header/app-header.component.spec.ts index 7ea9ad2..490967d 100644 --- a/src/app/app-header/app-header.component.spec.ts +++ b/src/app/app-header/app-header.component.spec.ts @@ -20,4 +20,28 @@ describe('AppHeaderComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe("timesTwoTestChange", () => { + it("should return the input value multiplied by two", () => { + let results = component.timesTwo(2); + + expect(results).toBe(4); + }); + }); + + describe("timesFourTestChange", () => { + it("should return the input value multiplied by four", () => { + let results = component.timesFour(2); + + expect(results).toBe(8); + }); + }); + + describe("timesEightTest", () => { + it("should return the input value multiplied by eight", () => { + let results = component.timesEight(2); + + expect(results).toBe(16); + }); + }); }); diff --git a/src/app/app-header/app-header.component.ts b/src/app/app-header/app-header.component.ts index 9a30cf8..05a34fa 100644 --- a/src/app/app-header/app-header.component.ts +++ b/src/app/app-header/app-header.component.ts @@ -11,5 +11,16 @@ export class AppHeaderComponent { content = new AppHeaderContent(); + timesTwo(entry: number): number { + return entry * 2; + } + + timesFour(entry: number): number { + return entry * 4; + } + + timesEight(entry: number): number { + return entry * 8; + } } diff --git a/src/app/app-home/app-home.component.spec.ts b/src/app/app-home/app-home.component.spec.ts index db9bbf8..a45293c 100644 --- a/src/app/app-home/app-home.component.spec.ts +++ b/src/app/app-home/app-home.component.spec.ts @@ -20,4 +20,29 @@ describe('AppHomeComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe("timesTwoTestChange", () => { + it("should return the input value multiplied by two", () => { + let results = component.timesTwo(2); + + expect(results).toBe(4); + }); + }); + + describe("timesFourTestChange", () => { + it("should return the input value multiplied by four", () => { + let results = component.timesFour(2); + + expect(results).toBe(8); + }); + }); + + describe("timesEightTest", () => { + it("should return the input value multiplied by eight", () => { + let results = component.timesEight(2); + + expect(results).toBe(16); + }); + }); + }); diff --git a/src/app/app-home/app-home.component.ts b/src/app/app-home/app-home.component.ts index d090cf6..13480b9 100644 --- a/src/app/app-home/app-home.component.ts +++ b/src/app/app-home/app-home.component.ts @@ -12,4 +12,15 @@ export class AppHomeComponent implements OnInit { ngOnInit(): void { } + timesTwo(entry: number): number { + return entry * 2; + } + + timesFour(entry: number): number { + return entry * 4; + } + + timesEight(entry: number): number { + return entry * 8; + } } diff --git a/src/app/reactive-form/reactive-form.component.spec.ts b/src/app/reactive-form/reactive-form.component.spec.ts index f7fc62a..bd6f1f5 100644 --- a/src/app/reactive-form/reactive-form.component.spec.ts +++ b/src/app/reactive-form/reactive-form.component.spec.ts @@ -22,7 +22,7 @@ describe('ReactiveFormComponent', () => { expect(component).toBeTruthy(); }); - describe("timesTwo", () => { + describe("timesTwoTestChange", () => { it("should return the input value multiplied by two", () => { let results = component.timesTwo(2); @@ -30,7 +30,7 @@ describe('ReactiveFormComponent', () => { }); }); - describe("timesFour", () => { + describe("timesFourTestChange", () => { it("should return the input value multiplied by four", () => { let results = component.timesFour(2); @@ -38,7 +38,7 @@ describe('ReactiveFormComponent', () => { }); }); - describe("timesEight", () => { + describe("timesEightTest", () => { it("should return the input value multiplied by eight", () => { let results = component.timesEight(2); diff --git a/stryker.config.mjs b/stryker.config.mjs index 7246427..41988b3 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -1,12 +1,7 @@ // @ts-check /** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ const config = { - mutate: [ - "src/**/*component.ts", - "!src/**/*.spec.ts", - "!src/test.ts", - "!src/environments/*.ts", - ], + mutate: process.env.FILESTOSTRYKE ? process.env.FILESTOSTRYKE.split(', ') : [], testRunner: "karma", karma: { configFile: "karma.conf.js",