From 12460b456cc9faa1c64e6a50eaaa873d08f8754d Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 2 Jul 2020 10:01:54 -0600 Subject: [PATCH] [QA] [Code Coverage] Add Three Dot Compare Url (#70525) --- .ci/Jenkinsfile_coverage | 9 +- .../ingest_coverage/__tests__/either.test.js | 20 ++-- .../__tests__/transforms.test.js | 10 +- .../integration_tests/ingest_coverage.test.js | 94 +++++++++++++------ .../code_coverage/ingest_coverage/maybe.js | 84 +++++++++++++++++ .../ingest_coverage/transforms.js | 50 +++++++--- .../shell_scripts/ingest_coverage.sh | 4 + vars/kibanaCoverage.groovy | 56 +++++++++-- 8 files changed, 268 insertions(+), 59 deletions(-) create mode 100644 src/dev/code_coverage/ingest_coverage/maybe.js diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index bd55bd73966ff..3986367d660a1 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -23,15 +23,22 @@ kibanaPipeline(timeoutMinutes: 240) { } def handleIngestion(timestamp) { + def previousSha = handlePreviousSha() kibanaPipeline.downloadCoverageArtifacts() kibanaCoverage.prokLinks("### Process HTML Links") kibanaCoverage.collectVcsInfo("### Collect VCS Info") kibanaCoverage.generateReports("### Merge coverage reports") kibanaCoverage.uploadCombinedReports() - kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, '### Ingest && Upload') + kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, '### Ingest && Upload') kibanaCoverage.uploadCoverageStaticSite(timestamp) } +def handlePreviousSha() { + def previous = kibanaCoverage.downloadPrevious('### Download OLD Previous') + kibanaCoverage.uploadPrevious('### Upload NEW Previous') + return previous +} + def handleFail() { def buildStatus = buildUtils.getBuildStatus() if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') { diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js index cce8fd3c2e62d..3a493539f6743 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js @@ -17,39 +17,39 @@ * under the License. */ -import { fromNullable, tryCatch, left, right } from '../either'; +import * as Either from '../either'; import { noop } from '../utils'; import expect from '@kbn/expect'; const pluck = (x) => (obj) => obj[x]; const expectNull = (x) => expect(x).to.equal(null); -const attempt = (obj) => fromNullable(obj).map(pluck('detail')); +const attempt = (obj) => Either.fromNullable(obj).map(pluck('detail')); describe(`either datatype functions`, () => { describe(`helpers`, () => { it(`'fromNullable' should be a fn`, () => { - expect(typeof fromNullable).to.be('function'); + expect(typeof Either.fromNullable).to.be('function'); }); - it(`'tryCatch' should be a fn`, () => { - expect(typeof tryCatch).to.be('function'); + it(`' Either.tryCatch' should be a fn`, () => { + expect(typeof Either.tryCatch).to.be('function'); }); it(`'left' should be a fn`, () => { - expect(typeof left).to.be('function'); + expect(typeof Either.left).to.be('function'); }); it(`'right' should be a fn`, () => { - expect(typeof right).to.be('function'); + expect(typeof Either.right).to.be('function'); }); }); - describe('tryCatch', () => { + describe(' Either.tryCatch', () => { let sut = undefined; it(`should return a 'Left' on error`, () => { - sut = tryCatch(() => { + sut = Either.tryCatch(() => { throw new Error('blah'); }); expect(sut.inspect()).to.be('Left(Error: blah)'); }); it(`should return a 'Right' on successful execution`, () => { - sut = tryCatch(noop); + sut = Either.tryCatch(noop); expect(sut.inspect()).to.be('Right(undefined)'); }); }); diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js index 2fd1d5cbe8d48..746bccc3d718a 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js @@ -18,7 +18,7 @@ */ import expect from '@kbn/expect'; -import { ciRunUrl, coveredFilePath, itemizeVcs } from '../transforms'; +import { ciRunUrl, coveredFilePath, itemizeVcs, prokPrevious } from '../transforms'; describe(`Transform fn`, () => { describe(`ciRunUrl`, () => { @@ -61,6 +61,14 @@ describe(`Transform fn`, () => { }); }); }); + describe(`prokPrevious`, () => { + const comparePrefixF = () => 'https://github.com/elastic/kibana/compare'; + process.env.FETCHED_PREVIOUS = 'A'; + it(`should return a previous compare url`, () => { + const actual = prokPrevious(comparePrefixF)('B'); + expect(actual).to.be(`https://github.com/elastic/kibana/compare/A...B`); + }); + }); describe(`itemizeVcs`, () => { it(`should return a sha url`, () => { const vcsInfo = [ diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js index 2a65839f85ac3..95056d9f0d8d7 100644 --- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js +++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js @@ -31,6 +31,7 @@ const env = { ES_HOST: 'https://super:changeme@some.fake.host:9243', NODE_ENV: 'integration_test', COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana', + FETCHED_PREVIOUS: 'FAKE_PREVIOUS_SHA', }; describe('Ingesting coverage', () => { @@ -68,31 +69,64 @@ describe('Ingesting coverage', () => { expect(folderStructure.test(actualUrl)).ok(); }); }); - describe(`vcsInfo`, () => { + let stdOutWithVcsInfo = ''; describe(`without a commit msg in the vcs info file`, () => { - let vcsInfo; - const args = [ - 'scripts/ingest_coverage.js', - '--verbose', - '--vcsInfoPath', - 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt', - '--path', - ]; - beforeAll(async () => { + const args = [ + 'scripts/ingest_coverage.js', + '--verbose', + '--vcsInfoPath', + 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt', + '--path', + ]; const opts = [...args, resolved]; const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - vcsInfo = stdout; + stdOutWithVcsInfo = stdout; }); it(`should be an obj w/o a commit msg`, () => { const commitMsgRE = /"commitMsg"/; - expect(commitMsgRE.test(vcsInfo)).to.not.be.ok(); + expect(commitMsgRE.test(stdOutWithVcsInfo)).to.not.be.ok(); + }); + }); + describe(`including previous sha`, () => { + let stdOutWithPrevious = ''; + beforeAll(async () => { + const opts = [...verboseArgs, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + stdOutWithPrevious = stdout; + }); + + it(`should have a vcsCompareUrl`, () => { + const previousCompareUrlRe = /vcsCompareUrl.+\s*.*https.+compare\/FAKE_PREVIOUS_SHA\.\.\.f07b34f6206/; + expect(previousCompareUrlRe.test(stdOutWithPrevious)).to.be.ok(); + }); + }); + describe(`with a commit msg in the vcs info file`, () => { + beforeAll(async () => { + const args = [ + 'scripts/ingest_coverage.js', + '--verbose', + '--vcsInfoPath', + 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt', + '--path', + ]; + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + stdOutWithVcsInfo = stdout; + }); + + it(`should be an obj w/ a commit msg`, () => { + const commitMsgRE = /commitMsg/; + expect(commitMsgRE.test(stdOutWithVcsInfo)).to.be.ok(); }); }); }); describe(`team assignment`, () => { + let shouldNotHavePipelineOut = ''; + let shouldIndeedHavePipelineOut = ''; + const args = [ 'scripts/ingest_coverage.js', '--verbose', @@ -101,26 +135,30 @@ describe('Ingesting coverage', () => { '--path', ]; - it(`should not occur when going to the totals index`, async () => { - const teamAssignRE = /"pipeline":/; - const shouldNotHavePipelineOut = await prokJustTotalOrNot(true, args); + const teamAssignRE = /pipeline:/; + + beforeAll(async () => { + const summaryPath = 'jest-combined/coverage-summary-just-total.json'; + const resolved = resolve(MOCKS_DIR, summaryPath); + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + shouldNotHavePipelineOut = stdout; + }); + beforeAll(async () => { + const summaryPath = 'jest-combined/coverage-summary-manual-mix.json'; + const resolved = resolve(MOCKS_DIR, summaryPath); + const opts = [...args, resolved]; + const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); + shouldIndeedHavePipelineOut = stdout; + }); + + it(`should not occur when going to the totals index`, () => { const actual = teamAssignRE.test(shouldNotHavePipelineOut); expect(actual).to.not.be.ok(); }); - it(`should indeed occur when going to the coverage index`, async () => { - const shouldIndeedHavePipelineOut = await prokJustTotalOrNot(false, args); - const onlyForTestingRe = /ingest-pipe=>team_assignment/; - const actual = onlyForTestingRe.test(shouldIndeedHavePipelineOut); + it(`should indeed occur when going to the coverage index`, () => { + const actual = /ingest-pipe=>team_assignment/.test(shouldIndeedHavePipelineOut); expect(actual).to.be.ok(); }); }); }); -async function prokJustTotalOrNot(isTotal, args) { - const justTotalPath = 'jest-combined/coverage-summary-just-total.json'; - const notJustTotalPath = 'jest-combined/coverage-summary-manual-mix.json'; - - const resolved = resolve(MOCKS_DIR, isTotal ? justTotalPath : notJustTotalPath); - const opts = [...args, resolved]; - const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env }); - return stdout; -} diff --git a/src/dev/code_coverage/ingest_coverage/maybe.js b/src/dev/code_coverage/ingest_coverage/maybe.js new file mode 100644 index 0000000000000..89936d6fc4b0e --- /dev/null +++ b/src/dev/code_coverage/ingest_coverage/maybe.js @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint new-cap: 0 */ +/* eslint no-unused-vars: 0 */ + +/** + * Just monad used for valid values + */ +export function Just(x) { + return { + value: () => x, + map: (f) => Maybe.of(f(x)), + isJust: () => true, + inspect: () => `Just(${x})`, + }; +} +Just.of = function of(x) { + return Just(x); +}; +export function just(x) { + return Just.of(x); +} + +/** + * Maybe monad. + * Maybe.fromNullable` lifts an `x` into either a `Just` + * or a `Nothing` typeclass. + */ +export function Maybe(x) { + return { + chain: (f) => f(x), + map: (f) => Maybe(f(x)), + inspect: () => `Maybe(${x})`, + nothing: () => Nothing(), + isNothing: () => false, + isJust: () => false, + }; +} +Maybe.of = function of(x) { + return just(x); +}; + +export function maybe(x) { + return Maybe.of(x); +} +export function fromNullable(x) { + return x !== null && x !== undefined && x !== false && x !== 'undefined' ? just(x) : nothing(); +} + +/** + * Nothing wraps undefined or null values and prevents errors + * that otherwise occur when mapping unexpected undefined or null + * values + */ +export function Nothing() { + return { + value: () => { + throw new TypeError(`Nothing algebraic data type returns...no value :)`); + }, + map: (f) => {}, + isNothing: () => true, + inspect: () => `[Nothing]`, + }; +} +export function nothing() { + return Nothing(); +} diff --git a/src/dev/code_coverage/ingest_coverage/transforms.js b/src/dev/code_coverage/ingest_coverage/transforms.js index 4cb6c2892c4f2..b8c9acd6fc49d 100644 --- a/src/dev/code_coverage/ingest_coverage/transforms.js +++ b/src/dev/code_coverage/ingest_coverage/transforms.js @@ -17,10 +17,11 @@ * under the License. */ -import { left, right, fromNullable } from './either'; +import * as Either from './either'; +import { fromNullable } from './maybe'; import { always, id, noop } from './utils'; -const maybeTotal = (x) => (x === 'total' ? left(x) : right(x)); +const maybeTotal = (x) => (x === 'total' ? Either.left(x) : Either.right(x)); const trimLeftFrom = (text, x) => x.substr(x.indexOf(text)); @@ -54,13 +55,13 @@ const root = (urlBase) => (ts) => (testRunnerType) => `${urlBase}/${ts}/${testRunnerType.toLowerCase()}-combined`; const prokForTotalsIndex = (mutateTrue) => (urlRoot) => (obj) => - right(obj) + Either.right(obj) .map(mutateTrue) .map(always(`${urlRoot}/index.html`)) .fold(noop, id); const prokForCoverageIndex = (root) => (mutateFalse) => (urlRoot) => (obj) => (siteUrl) => - right(siteUrl) + Either.right(siteUrl) .map((x) => { mutateFalse(obj); return x; @@ -87,7 +88,7 @@ export const coveredFilePath = (obj) => { const withoutCoveredFilePath = always(obj); const leadingSlashRe = /^\//; - const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? right(x) : left(x)); + const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? Either.right(x) : Either.left(x)); const dropLeadingSlash = (x) => x.replace(leadingSlashRe, ''); const dropRoot = (root) => (x) => maybeDropLeadingSlash(x.replace(root, '')).fold(id, dropLeadingSlash); @@ -97,11 +98,23 @@ export const coveredFilePath = (obj) => { }; export const ciRunUrl = (obj) => - fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ ...obj, ciRunUrl })); + Either.fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ + ...obj, + ciRunUrl, + })); const size = 50; -const truncateMsg = (msg) => (msg.length > size ? `${msg.slice(0, 50)}...` : msg); - +const truncateMsg = (msg) => { + const res = msg.length > size ? `${msg.slice(0, 50)}...` : msg; + return res; +}; +const comparePrefix = () => 'https://github.com/elastic/kibana/compare'; +export const prokPrevious = (comparePrefixF) => (currentSha) => { + return Either.fromNullable(process.env.FETCHED_PREVIOUS).fold( + noop, + (previousSha) => `${comparePrefixF()}/${previousSha}...${currentSha}` + ); +}; export const itemizeVcs = (vcsInfo) => (obj) => { const [branch, sha, author, commitMsg] = vcsInfo; @@ -111,12 +124,23 @@ export const itemizeVcs = (vcsInfo) => (obj) => { author, vcsUrl: `https://github.com/elastic/kibana/commit/${sha}`, }; - const res = fromNullable(commitMsg).fold(always({ ...obj, vcs }), (msg) => ({ - ...obj, - vcs: { ...vcs, commitMsg: truncateMsg(msg) }, - })); - return res; + const mutateVcs = (x) => (vcs.commitMsg = truncateMsg(x)); + fromNullable(commitMsg).map(mutateVcs); + + const vcsCompareUrl = process.env.FETCHED_PREVIOUS + ? `${comparePrefix()}/${process.env.FETCHED_PREVIOUS}...${sha}` + : 'PREVIOUS SHA NOT PROVIDED'; + + // const withoutPreviousL = always({ ...obj, vcs }); + const withPreviousR = () => ({ + ...obj, + vcs: { + ...vcs, + vcsCompareUrl, + }, + }); + return withPreviousR(); }; export const testRunner = (obj) => { const { jsonSummaryPath } = obj; diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh index d3cf31fc0f427..0b67dac307473 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh @@ -14,6 +14,10 @@ CI_RUN_URL=$3 export CI_RUN_URL echo "### debug CI_RUN_URL: ${CI_RUN_URL}" +FETCHED_PREVIOUS=$4 +export FETCHED_PREVIOUS +echo "### debug FETCHED_PREVIOUS: ${FETCHED_PREVIOUS}" + ES_HOST="https://${USER_FROM_VAULT}:${PASS_FROM_VAULT}@${HOST_FROM_VAULT}" export ES_HOST diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index e511d7a8fc15e..66ebe3478fbec 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -1,3 +1,46 @@ +def downloadPrevious(title) { + def vaultSecret = 'secret/gce/elastic-bekitzur/service-account/kibana' + + withGcpServiceAccount.fromVaultSecret(vaultSecret, 'value') { + kibanaPipeline.bash(''' + + gsutil -m cp -r gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/previous.txt . || echo "### Previous Pointer NOT FOUND?" + + if [ -e ./previous.txt ]; then + mv previous.txt downloaded_previous.txt + echo "### downloaded_previous.txt" + cat downloaded_previous.txt + fi + + ''', title) + + def previous = sh(script: 'cat downloaded_previous.txt', label: '### Capture Previous Sha', returnStdout: true).trim() + + return previous + } +} + +def uploadPrevious(title) { + def vaultSecret = 'secret/gce/elastic-bekitzur/service-account/kibana' + + withGcpServiceAccount.fromVaultSecret(vaultSecret, 'value') { + kibanaPipeline.bash(''' + + collectPrevious() { + PREVIOUS=$(git log --pretty=format:%h -1) + echo "### PREVIOUS: ${PREVIOUS}" + echo $PREVIOUS > previous.txt + } + collectPrevious + + gsutil cp previous.txt gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/ + + + ''', title) + + } +} + def uploadCoverageStaticSite(timestamp) { def uploadPrefix = "gs://elastic-bekitzur-kibana-coverage-live/" def uploadPrefixWithTimeStamp = "${uploadPrefix}${timestamp}/" @@ -67,6 +110,7 @@ EOF cat src/dev/code_coverage/www/index.html ''', "### Combine Index Partials") } + def collectVcsInfo(title) { kibanaPipeline.bash(''' predicate() { @@ -125,31 +169,31 @@ def uploadCombinedReports() { ) } -def ingestData(jobName, buildNum, buildUrl, title) { +def ingestData(jobName, buildNum, buildUrl, previousSha, title) { kibanaPipeline.bash(""" source src/dev/ci_setup/setup_env.sh yarn kbn bootstrap --prefer-offline # Using existing target/kibana-coverage folder - . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' + . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' ${previousSha} """, title) } -def ingestWithVault(jobName, buildNum, buildUrl, title) { +def ingestWithVault(jobName, buildNum, buildUrl, previousSha, title) { def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch' withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') { withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') { - ingestData(jobName, buildNum, buildUrl, title) + ingestData(jobName, buildNum, buildUrl, previousSha, title) } } } } -def ingest(jobName, buildNumber, buildUrl, timestamp, title) { +def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, title) { withEnv([ "TIME_STAMP=${timestamp}", ]) { - ingestWithVault(jobName, buildNumber, buildUrl, title) + ingestWithVault(jobName, buildNumber, buildUrl, previousSha, title) } }