mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Generate acceptance entry points incrementally
Every acceptance run re-parsed and regenerated all features even when only one changed. Record feature_hash and aps_commit in the per-feature metadata and skip parse/generate when both match, and drop generated tests for features that no longer exist. By specifier.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
'use strict'
|
||||
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const crypto = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
@@ -46,6 +47,49 @@ function ensureAps () {
|
||||
sh('git', ['clone', '--depth', '1', APS_URL, apsDir])
|
||||
}
|
||||
|
||||
function apsCommit () {
|
||||
try {
|
||||
return sh('git', ['-C', apsDir, 'rev-parse', 'HEAD']).trim()
|
||||
} catch (err) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function featureHash (featurePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(featurePath)).digest('hex')
|
||||
}
|
||||
|
||||
// True when the generated entry point and metadata already match the current
|
||||
// feature text and APS checkout, so parse/generate can be skipped.
|
||||
function isUpToDate (featurePath, base, commit) {
|
||||
const testFile = path.join(genDir, `${base}.acceptance.test.js`)
|
||||
const metaFile = path.join(genDir, 'metadata', `${base}.json`)
|
||||
if (!fs.existsSync(testFile) || !fs.existsSync(metaFile)) return false
|
||||
try {
|
||||
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'))
|
||||
return meta.feature_hash === featureHash(featurePath) && meta.aps_commit === commit
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Remove generated tests for features that no longer exist under specs/.
|
||||
function removeStaleGeneratedTests (features) {
|
||||
const bases = new Set(features.map((f) => f.replace(/\.feature$/i, '')))
|
||||
if (!fs.existsSync(genDir)) return
|
||||
for (const file of fs.readdirSync(genDir)) {
|
||||
if (!file.endsWith('.acceptance.test.js')) continue
|
||||
const base = file.replace(/\.acceptance\.test\.js$/, '')
|
||||
if (!bases.has(base)) {
|
||||
try {
|
||||
fs.rmSync(path.join(genDir, file), { force: true })
|
||||
} catch (err) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
ensureAps()
|
||||
|
||||
@@ -61,17 +105,23 @@ function main () {
|
||||
|
||||
fs.mkdirSync(irDir, { recursive: true })
|
||||
fs.mkdirSync(genDir, { recursive: true })
|
||||
removeStaleGeneratedTests(features)
|
||||
|
||||
const commit = apsCommit()
|
||||
|
||||
for (const featureFile of features) {
|
||||
const base = featureFile.replace(/\.feature$/i, '')
|
||||
const featurePath = path.join(specsDir, featureFile)
|
||||
|
||||
if (isUpToDate(featurePath, base, commit)) continue
|
||||
|
||||
const irPath = path.join(irDir, `${base}.json`)
|
||||
|
||||
// 1) Parse the feature to JSON IR using the Babashka APS gherkin-parser.
|
||||
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
||||
|
||||
// 2) Generate executable acceptance entry points from the IR.
|
||||
sh('node', [path.join(root, 'acceptance', 'lib', 'generate.js'), irPath, genDir])
|
||||
sh('node', [path.join(root, 'acceptance', 'lib', 'generate.js'), irPath, genDir, featurePath, commit])
|
||||
}
|
||||
|
||||
// 3) Run every generated acceptance test.
|
||||
|
||||
@@ -37,12 +37,22 @@ function relativeRequire (fromDir, targetFile) {
|
||||
return rel
|
||||
}
|
||||
|
||||
function hashFile (file) {
|
||||
try {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
const irArg = process.argv[2]
|
||||
const outArg = process.argv[3]
|
||||
const featureArg = process.argv[4]
|
||||
const apsCommit = process.argv[5]
|
||||
|
||||
if (!irArg || !outArg) {
|
||||
console.error('usage: acceptance-entrypoint-generator <json-ir> <generated-test-output-dir>')
|
||||
console.error('usage: acceptance-entrypoint-generator <json-ir> <generated-test-output-dir> [feature-file] [aps-commit]')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
@@ -99,7 +109,9 @@ main().catch((err) => { console.error(err); process.exit(1) })
|
||||
ir_path: path.resolve(irArg),
|
||||
implementation_hash: `sha256:${hash}`,
|
||||
hash_scope: 'generated_files',
|
||||
generated_files: [testFile]
|
||||
generated_files: [testFile],
|
||||
feature_hash: featureArg ? hashFile(featureArg) : null,
|
||||
aps_commit: apsCommit || null
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(metaDir, metadataName(featureKey)),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -50,6 +51,49 @@ function cleanStaleWorlds () {
|
||||
}
|
||||
}
|
||||
|
||||
function apsCommit () {
|
||||
try {
|
||||
return sh('git', ['-C', apsDir, 'rev-parse', 'HEAD']).trim()
|
||||
} catch (err) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function featureHash (featurePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(featurePath)).digest('hex')
|
||||
}
|
||||
|
||||
// True when the generated entry point and metadata already match the current
|
||||
// feature text and APS checkout, so parse/generate can be skipped.
|
||||
function isUpToDate (featurePath, base, commit) {
|
||||
const testFile = path.join(genDir, `${base}.acceptance.test.js`)
|
||||
const metaFile = path.join(genDir, 'metadata', `${base}.json`)
|
||||
if (!fs.existsSync(testFile) || !fs.existsSync(metaFile)) return false
|
||||
try {
|
||||
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'))
|
||||
return meta.feature_hash === featureHash(featurePath) && meta.aps_commit === commit
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Remove generated tests for features that no longer exist under specs/.
|
||||
function removeStaleGeneratedTests (features) {
|
||||
const bases = new Set(features.map((f) => f.replace(/\.feature$/i, '')))
|
||||
if (!fs.existsSync(genDir)) return
|
||||
for (const file of fs.readdirSync(genDir)) {
|
||||
if (!file.endsWith('.acceptance.test.js')) continue
|
||||
const base = file.replace(/\.acceptance\.test\.js$/, '')
|
||||
if (!bases.has(base)) {
|
||||
try {
|
||||
fs.rmSync(path.join(genDir, file), { force: true })
|
||||
} catch (err) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
cleanStaleWorlds()
|
||||
ensureAps()
|
||||
@@ -66,14 +110,20 @@ function main () {
|
||||
|
||||
fs.mkdirSync(irDir, { recursive: true })
|
||||
fs.mkdirSync(genDir, { recursive: true })
|
||||
removeStaleGeneratedTests(features)
|
||||
|
||||
const commit = apsCommit()
|
||||
|
||||
for (const featureFile of features) {
|
||||
const base = featureFile.replace(/\.feature$/i, '')
|
||||
const featurePath = path.join(specsDir, featureFile)
|
||||
|
||||
if (isUpToDate(featurePath, base, commit)) continue
|
||||
|
||||
const irPath = path.join(irDir, `${base}.json`)
|
||||
|
||||
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
||||
sh('node', [path.join(__dirname, 'lib', 'generate.js'), irPath, genDir])
|
||||
sh('node', [path.join(__dirname, 'lib', 'generate.js'), irPath, genDir, featurePath, commit])
|
||||
}
|
||||
|
||||
const tests = fs
|
||||
|
||||
@@ -27,12 +27,22 @@ function relativeImport (fromDir, targetFile) {
|
||||
return rel
|
||||
}
|
||||
|
||||
function hashFile (file) {
|
||||
try {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
const irArg = process.argv[2]
|
||||
const outArg = process.argv[3]
|
||||
const featureArg = process.argv[4]
|
||||
const apsCommit = process.argv[5]
|
||||
|
||||
if (!irArg || !outArg) {
|
||||
console.error('usage: acceptance-entrypoint-generator <json-ir> <generated-test-output-dir>')
|
||||
console.error('usage: acceptance-entrypoint-generator <json-ir> <generated-test-output-dir> [feature-file] [aps-commit]')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
@@ -87,7 +97,9 @@ main().catch((err) => { console.error(err); process.exit(1) })
|
||||
ir_path: path.resolve(irArg),
|
||||
implementation_hash: `sha256:${hash}`,
|
||||
hash_scope: 'generated_files',
|
||||
generated_files: [testFile]
|
||||
generated_files: [testFile],
|
||||
feature_hash: featureArg ? hashFile(featureArg) : null,
|
||||
aps_commit: apsCommit || null
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(metaDir, metadataName(featureKey)),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -35,6 +36,49 @@ function ensureAps () {
|
||||
sh('git', ['clone', '--depth', '1', APS_URL, apsDir])
|
||||
}
|
||||
|
||||
function apsCommit () {
|
||||
try {
|
||||
return sh('git', ['-C', apsDir, 'rev-parse', 'HEAD']).trim()
|
||||
} catch (err) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function featureHash (featurePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(featurePath)).digest('hex')
|
||||
}
|
||||
|
||||
// True when the generated entry point and metadata already match the current
|
||||
// feature text and APS checkout, so parse/generate can be skipped.
|
||||
function isUpToDate (featurePath, base, commit) {
|
||||
const testFile = path.join(genDir, `${base}.acceptance.test.js`)
|
||||
const metaFile = path.join(genDir, 'metadata', `${base}.json`)
|
||||
if (!fs.existsSync(testFile) || !fs.existsSync(metaFile)) return false
|
||||
try {
|
||||
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'))
|
||||
return meta.feature_hash === featureHash(featurePath) && meta.aps_commit === commit
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Remove generated tests for features that no longer exist under specs/.
|
||||
function removeStaleGeneratedTests (features) {
|
||||
const bases = new Set(features.map((f) => f.replace(/\.feature$/i, '')))
|
||||
if (!fs.existsSync(genDir)) return
|
||||
for (const file of fs.readdirSync(genDir)) {
|
||||
if (!file.endsWith('.acceptance.test.js')) continue
|
||||
const base = file.replace(/\.acceptance\.test\.js$/, '')
|
||||
if (!bases.has(base)) {
|
||||
try {
|
||||
fs.rmSync(path.join(genDir, file), { force: true })
|
||||
} catch (err) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
ensureAps()
|
||||
|
||||
@@ -50,14 +94,20 @@ function main () {
|
||||
|
||||
fs.mkdirSync(irDir, { recursive: true })
|
||||
fs.mkdirSync(genDir, { recursive: true })
|
||||
removeStaleGeneratedTests(features)
|
||||
|
||||
const commit = apsCommit()
|
||||
|
||||
for (const featureFile of features) {
|
||||
const base = featureFile.replace(/\.feature$/i, '')
|
||||
const featurePath = path.join(specsDir, featureFile)
|
||||
|
||||
if (isUpToDate(featurePath, base, commit)) continue
|
||||
|
||||
const irPath = path.join(irDir, `${base}.json`)
|
||||
|
||||
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
||||
sh('node', [path.join(__dirname, 'lib', 'generate.js'), irPath, genDir])
|
||||
sh('node', [path.join(__dirname, 'lib', 'generate.js'), irPath, genDir, featurePath, commit])
|
||||
}
|
||||
|
||||
const tests = fs
|
||||
|
||||
@@ -23,12 +23,22 @@ function relativeImport (fromDir, targetFile) {
|
||||
return rel
|
||||
}
|
||||
|
||||
function hashFile (file) {
|
||||
try {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
const irArg = process.argv[2]
|
||||
const outArg = process.argv[3]
|
||||
const featureArg = process.argv[4]
|
||||
const apsCommit = process.argv[5]
|
||||
|
||||
if (!irArg || !outArg) {
|
||||
console.error('usage: acceptance-entrypoint-generator <json-ir> <generated-test-output-dir>')
|
||||
console.error('usage: acceptance-entrypoint-generator <json-ir> <generated-test-output-dir> [feature-file] [aps-commit]')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
@@ -83,7 +93,9 @@ main().catch((err) => { console.error(err); process.exit(1) })
|
||||
ir_path: path.resolve(irArg),
|
||||
implementation_hash: `sha256:${hash}`,
|
||||
hash_scope: 'generated_files',
|
||||
generated_files: [testFile]
|
||||
generated_files: [testFile],
|
||||
feature_hash: featureArg ? hashFile(featureArg) : null,
|
||||
aps_commit: apsCommit || null
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(metaDir, metadataName(featureKey)),
|
||||
|
||||
Reference in New Issue
Block a user