Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/cli/e2e/__tests__/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,50 @@ describe('test', { timeout: 45000 }, () => {
}
}, 120_000)
})

describe('shared-constructs-project', () => {
let fixt: FixtureSandbox

beforeAll(async () => {
fixt = await FixtureSandbox.create({
// The parser's sandbox spec covers the same fixture; it lives with the
// parser fixtures so there is one copy.
source: path.join(
__dirname, '..', '..', 'src', 'services', '__tests__', 'project-parser-fixtures', 'shared-constructs-project',
),
template: 'playwright',
})
}, 180_000)

afterAll(async () => {
await fixt?.destroy()
})

it('Should list a check under the module that declares it, not under the check file importing it', async () => {
const result = await runTest(fixt, ['--list', 'shared/checks'])
expect(result.stdout).toContain('src/shared/checks.ts')
expect(result.stdout).toContain('Shared API')
expect(result.stdout).not.toContain('src/b.check.ts')
})

it('Should select a check made by a helper when naming the check file that called it', async () => {
const result = await runTest(fixt, ['--list', 'factory.check'])
expect(result.stdout).toContain('src/lib/factory.ts')
expect(result.stdout).toContain('factory-browser')
})

it('Should select the checks a check file loads from imported modules, listed under their own files', async () => {
const result = await runTest(fixt, ['--list', 'b.check'])
expect(result.stdout).toContain('src/b.check.ts')
expect(result.stdout).toContain('src/shared/checks.ts')
expect(result.stdout).toContain('Shared API')
})

it('Should not select checks from modules the named check file does not load', async () => {
const result = await runTest(fixt, ['--list', 'a.check'])
expect(result.stdout).toContain('src/a.check.ts')
expect(result.stdout).not.toContain('Shared API')
expect(result.stdout).not.toContain('Shared browser')
})
})
})
9 changes: 7 additions & 2 deletions packages/cli/src/commands/debug/parse-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Diagnostics,
Session,
} from '../../constructs/index.js'
import { splitConfigFilePath } from '../../services/util.js'
import { getGitRepoRoot, splitConfigFilePath } from '../../services/util.js'
import commonMessages from '../../messages/common-messages.js'
import { loadSnapshot, Runtime } from '../../runtimes/index.js'
import { Bundler } from '../../services/check-parser/bundler.js'
Expand Down Expand Up @@ -37,6 +37,9 @@ export type ParseProjectOutput = {
type: string
member: boolean
payload: unknown
// Present when the project lives in a git repository: the declaring
// file relative to the repository root, as a deploy sends it.
sourceFile?: string
}[]
} | null
}
Expand Down Expand Up @@ -223,7 +226,9 @@ export default class ParseProjectCommand extends Command {
bundleMs = performance.now() - bundleStartedAt

const synthesizeStartedAt = performance.now()
const synthesized = bundle.synthesize()
// Same envelope as `checkly deploy` sends, including the per-resource
// `sourceFile` relative to the git repository root.
const synthesized = bundle.synthesize({ repoRoot: getGitRepoRoot() })
synthesizeMs = performance.now() - synthesizeStartedAt
return synthesized
})()
Expand Down
30 changes: 13 additions & 17 deletions packages/cli/src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../services/abstract-check-runner.js'
import TestRunner from '../services/test-runner.js'
import { loadChecklyConfig, resolveDependencyCacheVersion } from '../services/checkly-config-loader.js'
import { filterByFileNamePattern, filterByCheckNamePattern, filterByTags } from '../services/test-filters.js'
import { filterByCheckFiles, filterByCheckNamePattern, filterByTags } from '../services/test-filters.js'
import { AuthCommand } from './authCommand.js'
import { BrowserCheck, Check, HeartbeatMonitor, MultiStepCheck, Project, RetryStrategyBuilder, RuntimeCheck, Session } from '../constructs/index.js'
import type { Region } from '../index.js'
Expand Down Expand Up @@ -214,22 +214,18 @@ export default class Test extends AuthCommand {
return false
}

let entrypointMatch = false
if (check instanceof BrowserCheck || check instanceof MultiStepCheck) {
// For historical reasons the path used for filtering has always
// been relative to the project base path.
const relativeEntrypoint = isEntrypoint(check.code)
? Session.relativePosixPath(check.code.entrypoint)
: undefined

if (relativeEntrypoint) {
if (filterByFileNamePattern(filePatterns, relativeEntrypoint)) {
entrypointMatch = true
}
}
}

if (!entrypointMatch && !filterByFileNamePattern(filePatterns, check.getSourceFile())) {
// For historical reasons the entrypoint used for filtering has
// always been relative to the project base path.
const scripted = check instanceof BrowserCheck || check instanceof MultiStepCheck
const entrypoint = scripted && isEntrypoint(check.code)
? Session.relativePosixPath(check.code.entrypoint)
: undefined
const fileMatch = filterByCheckFiles(filePatterns, {
sourceFile: check.getSourceFile(),
loadedFrom: Session.relativeCheckFilePath(check.loadingFileAbsolutePath),
entrypoint,
})
if (!fileMatch) {
return false
}

Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/constructs/__tests__/check.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from 'node:path'

import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import { Frequency } from '../frequency.js'
Expand All @@ -17,6 +19,28 @@ describe('Check', () => {
Session.reset()
})

describe('__checkFilePath', () => {
const request = { method: 'GET' as const, url: 'https://api.example.com/health' }

it('is the declaring file relative to the parse directory', () => {
// Created from a test, so the declaring file is the session's current
// check file.
Session.checkFilesDirectory = path.resolve('/proj')
Session.checkFileAbsolutePath = path.resolve('/proj/src/a.check.ts')
const check = new ApiCheck('a', { name: 'A', request })
expect(check.checkFileAbsolutePath).toBe(path.resolve('/proj/src/a.check.ts'))
expect(check.__checkFilePath).toBe('src/a.check.ts')
expect(check.getSourceFile()).toBe('src/a.check.ts')
})

it('is unset while no project is being parsed, as for constructs in the config file', () => {
Session.checkFileAbsolutePath = path.resolve('/proj/checkly.config.ts')
const check = new ApiCheck('a', { name: 'A', request })
expect(check.checkFileAbsolutePath).toBe(path.resolve('/proj/checkly.config.ts'))
expect(check.__checkFilePath).toBeUndefined()
})
})

it('synthesizes Frequency instances as numeric frequency fields', () => {
const check = new ApiCheck('api-health', {
name: 'API Health',
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/src/constructs/__tests__/session.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import path from 'node:path'

import { afterEach, describe, expect, it } from 'vitest'

import { Session } from '../session.js'

describe('Session.relativeCheckFilePath', () => {
afterEach(() => {
Session.reset()
})

it('is relative to the parse directory, with posix separators', () => {
Session.checkFilesDirectory = path.resolve('/proj')
expect(Session.relativeCheckFilePath(path.resolve('/proj/src/a.check.ts'))).toBe('src/a.check.ts')
})

it('walks up for a declaring file outside the parse directory', () => {
Session.checkFilesDirectory = path.resolve('/repo/apps/web')
expect(Session.relativeCheckFilePath(path.resolve('/repo/packages/checks/index.ts')))
.toBe('../../packages/checks/index.ts')
})

it('is unset without a parse directory or a file', () => {
expect(Session.relativeCheckFilePath(path.resolve('/proj/src/a.check.ts'))).toBeUndefined()
Session.checkFilesDirectory = path.resolve('/proj')
expect(Session.relativeCheckFilePath(undefined)).toBeUndefined()
})
})
45 changes: 36 additions & 9 deletions packages/cli/src/constructs/check-group-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import path from 'node:path'

import { glob } from 'glob'

import { pathToPosix } from '../services/util.js'

import { AlertChannel, AlertChannelRef } from './alert-channel.js'
import { EnvironmentVariable } from './environment-variable.js'
import { PrivateLocation, PrivateLocationRef } from './private-location.js'
Expand Down Expand Up @@ -41,14 +43,20 @@ const defaultApiCheckDefaults: ApiCheckDefaultConfig = {

type BrowserCheckConfig = CheckConfigDefaults & {
/**
* Glob pattern to include multiple files, i.e. all `.spec.ts` files
* Glob pattern to include multiple files, i.e. all `.spec.ts` files,
* relative to the check file being loaded when the group is created.
* When nothing matches there and the group is declared in another file
* (a shared module, a helper), that file's directory is searched.
*/
testMatch: string | string[]
}

type MultiStepCheckConfig = CheckConfigDefaults & {
/**
* Glob pattern to include multiple files, i.e. all `.spec.ts` files
* Glob pattern to include multiple files, i.e. all `.spec.ts` files,
* relative to the check file being loaded when the group is created.
* When nothing matches there and the group is declared in another file
* (a shared module, a helper), that file's directory is searched.
*/
testMatch: string | string[]
}
Expand Down Expand Up @@ -344,13 +352,12 @@ export class CheckGroupV1 extends Construct {
this.runParallel = props.runParallel
// `browserChecks` is not a CheckGroup resource property. Not present in synthesize()
this.browserChecks = props.browserChecks
const fileAbsolutePath = Session.checkFileAbsolutePath!
if (props.browserChecks?.testMatch) {
this.__addChecks(fileAbsolutePath, props.browserChecks.testMatch, CheckTypes.BROWSER)
this.__addChecks(props.browserChecks.testMatch, CheckTypes.BROWSER)
}
this.multiStepChecks = props.multiStepChecks
if (props.multiStepChecks?.testMatch) {
this.__addChecks(fileAbsolutePath, props.multiStepChecks.testMatch, CheckTypes.MULTI_STEP)
this.__addChecks(props.multiStepChecks.testMatch, CheckTypes.MULTI_STEP)
}
Session.registerConstruct(this)
this.__addSubscriptions()
Expand Down Expand Up @@ -423,13 +430,28 @@ export class CheckGroupV1 extends Construct {
return new CheckGroupRef(`check-group-${id}`, id)
}

/**
* Creates a check for every file `testMatch` finds next to the check
* file that was being loaded when the group was created (as it always
* did) or, when nothing matches there, next to the file that declares
* the group.
*/
private __addChecks (
fileAbsolutePath: string,
testMatch: string | string[],
checkType: typeof CheckTypes.BROWSER | typeof CheckTypes.MULTI_STEP,
) {
const parent = path.dirname(fileAbsolutePath)
const matched = glob.sync(testMatch, { nodir: true, cwd: parent })
const roots = [...new Set([this.loadingFileAbsolutePath, this.checkFileAbsolutePath])]
.filter((file): file is string => file !== undefined)
.map(file => path.dirname(file))
let parent = roots[0]
let matched: string[] = []
for (const root of roots) {
matched = glob.sync(testMatch, { nodir: true, cwd: root })
if (matched.length > 0) {
parent = root
break
}
}
for (const match of matched) {
const filepath = path.join(parent, match)
const props = {
Expand All @@ -440,7 +462,12 @@ export class CheckGroupV1 extends Construct {
},
// the browserChecks props inherited from the group are applied in BrowserCheck.constructor()
}
const checkLogicalId = Session.relativePosixPath(filepath)
// Before a project is parsed (a group declared in the Checkly config
// file) there is no base path; the check constructor rejects the
// construct anyway, with a message naming the config file.
const checkLogicalId = Session.basePath
? Session.relativePosixPath(filepath)
: pathToPosix(match)
if (checkType === CheckTypes.BROWSER) {
new BrowserCheck(checkLogicalId, props)
} else {
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/constructs/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,12 @@ export abstract class Check extends Construct {
useGlobalAlertSettings?: boolean
runParallel?: boolean
triggerIncident?: IncidentTrigger
__checkFilePath?: string // internal variable to filter by check file name from the CLI
/**
* The declaring file relative to the directory the project is parsed
* from; `checkly test <file>` filters on it and reporters group by it.
* Internal.
*/
__checkFilePath?: string
#intent?: CheckIntent | null
#aiAutoRepairEnabled?: boolean | null

Expand Down Expand Up @@ -408,7 +413,7 @@ export abstract class Check extends Construct {
this.useGlobalAlertSettings = !this.alertSettings
this.runParallel = config.runParallel ?? false
this.triggerIncident = config.triggerIncident
this.__checkFilePath = Session.checkFilePath
this.__checkFilePath = Session.relativeCheckFilePath(this.checkFileAbsolutePath)
}

protected async validateDoubleCheck (diagnostics: Diagnostics): Promise<void> {
Expand Down
Loading
Loading