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'
|
'use strict'
|
||||||
|
|
||||||
const { execFileSync } = require('node:child_process')
|
const { execFileSync } = require('node:child_process')
|
||||||
|
const crypto = require('node:crypto')
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
|
|
||||||
@@ -46,6 +47,49 @@ function ensureAps () {
|
|||||||
sh('git', ['clone', '--depth', '1', APS_URL, apsDir])
|
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 () {
|
function main () {
|
||||||
ensureAps()
|
ensureAps()
|
||||||
|
|
||||||
@@ -61,17 +105,23 @@ function main () {
|
|||||||
|
|
||||||
fs.mkdirSync(irDir, { recursive: true })
|
fs.mkdirSync(irDir, { recursive: true })
|
||||||
fs.mkdirSync(genDir, { recursive: true })
|
fs.mkdirSync(genDir, { recursive: true })
|
||||||
|
removeStaleGeneratedTests(features)
|
||||||
|
|
||||||
|
const commit = apsCommit()
|
||||||
|
|
||||||
for (const featureFile of features) {
|
for (const featureFile of features) {
|
||||||
const base = featureFile.replace(/\.feature$/i, '')
|
const base = featureFile.replace(/\.feature$/i, '')
|
||||||
const featurePath = path.join(specsDir, featureFile)
|
const featurePath = path.join(specsDir, featureFile)
|
||||||
|
|
||||||
|
if (isUpToDate(featurePath, base, commit)) continue
|
||||||
|
|
||||||
const irPath = path.join(irDir, `${base}.json`)
|
const irPath = path.join(irDir, `${base}.json`)
|
||||||
|
|
||||||
// 1) Parse the feature to JSON IR using the Babashka APS gherkin-parser.
|
// 1) Parse the feature to JSON IR using the Babashka APS gherkin-parser.
|
||||||
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
||||||
|
|
||||||
// 2) Generate executable acceptance entry points from the IR.
|
// 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.
|
// 3) Run every generated acceptance test.
|
||||||
|
|||||||
@@ -37,12 +37,22 @@ function relativeRequire (fromDir, targetFile) {
|
|||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hashFile (file) {
|
||||||
|
try {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function main () {
|
function main () {
|
||||||
const irArg = process.argv[2]
|
const irArg = process.argv[2]
|
||||||
const outArg = process.argv[3]
|
const outArg = process.argv[3]
|
||||||
|
const featureArg = process.argv[4]
|
||||||
|
const apsCommit = process.argv[5]
|
||||||
|
|
||||||
if (!irArg || !outArg) {
|
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)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +109,9 @@ main().catch((err) => { console.error(err); process.exit(1) })
|
|||||||
ir_path: path.resolve(irArg),
|
ir_path: path.resolve(irArg),
|
||||||
implementation_hash: `sha256:${hash}`,
|
implementation_hash: `sha256:${hash}`,
|
||||||
hash_scope: 'generated_files',
|
hash_scope: 'generated_files',
|
||||||
generated_files: [testFile]
|
generated_files: [testFile],
|
||||||
|
feature_hash: featureArg ? hashFile(featureArg) : null,
|
||||||
|
aps_commit: apsCommit || null
|
||||||
}
|
}
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(metaDir, metadataName(featureKey)),
|
path.join(metaDir, metadataName(featureKey)),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process'
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
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 () {
|
function main () {
|
||||||
cleanStaleWorlds()
|
cleanStaleWorlds()
|
||||||
ensureAps()
|
ensureAps()
|
||||||
@@ -66,14 +110,20 @@ function main () {
|
|||||||
|
|
||||||
fs.mkdirSync(irDir, { recursive: true })
|
fs.mkdirSync(irDir, { recursive: true })
|
||||||
fs.mkdirSync(genDir, { recursive: true })
|
fs.mkdirSync(genDir, { recursive: true })
|
||||||
|
removeStaleGeneratedTests(features)
|
||||||
|
|
||||||
|
const commit = apsCommit()
|
||||||
|
|
||||||
for (const featureFile of features) {
|
for (const featureFile of features) {
|
||||||
const base = featureFile.replace(/\.feature$/i, '')
|
const base = featureFile.replace(/\.feature$/i, '')
|
||||||
const featurePath = path.join(specsDir, featureFile)
|
const featurePath = path.join(specsDir, featureFile)
|
||||||
|
|
||||||
|
if (isUpToDate(featurePath, base, commit)) continue
|
||||||
|
|
||||||
const irPath = path.join(irDir, `${base}.json`)
|
const irPath = path.join(irDir, `${base}.json`)
|
||||||
|
|
||||||
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
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
|
const tests = fs
|
||||||
|
|||||||
@@ -27,12 +27,22 @@ function relativeImport (fromDir, targetFile) {
|
|||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hashFile (file) {
|
||||||
|
try {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function main () {
|
function main () {
|
||||||
const irArg = process.argv[2]
|
const irArg = process.argv[2]
|
||||||
const outArg = process.argv[3]
|
const outArg = process.argv[3]
|
||||||
|
const featureArg = process.argv[4]
|
||||||
|
const apsCommit = process.argv[5]
|
||||||
|
|
||||||
if (!irArg || !outArg) {
|
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)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +97,9 @@ main().catch((err) => { console.error(err); process.exit(1) })
|
|||||||
ir_path: path.resolve(irArg),
|
ir_path: path.resolve(irArg),
|
||||||
implementation_hash: `sha256:${hash}`,
|
implementation_hash: `sha256:${hash}`,
|
||||||
hash_scope: 'generated_files',
|
hash_scope: 'generated_files',
|
||||||
generated_files: [testFile]
|
generated_files: [testFile],
|
||||||
|
feature_hash: featureArg ? hashFile(featureArg) : null,
|
||||||
|
aps_commit: apsCommit || null
|
||||||
}
|
}
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(metaDir, metadataName(featureKey)),
|
path.join(metaDir, metadataName(featureKey)),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process'
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
@@ -35,6 +36,49 @@ function ensureAps () {
|
|||||||
sh('git', ['clone', '--depth', '1', APS_URL, apsDir])
|
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 () {
|
function main () {
|
||||||
ensureAps()
|
ensureAps()
|
||||||
|
|
||||||
@@ -50,14 +94,20 @@ function main () {
|
|||||||
|
|
||||||
fs.mkdirSync(irDir, { recursive: true })
|
fs.mkdirSync(irDir, { recursive: true })
|
||||||
fs.mkdirSync(genDir, { recursive: true })
|
fs.mkdirSync(genDir, { recursive: true })
|
||||||
|
removeStaleGeneratedTests(features)
|
||||||
|
|
||||||
|
const commit = apsCommit()
|
||||||
|
|
||||||
for (const featureFile of features) {
|
for (const featureFile of features) {
|
||||||
const base = featureFile.replace(/\.feature$/i, '')
|
const base = featureFile.replace(/\.feature$/i, '')
|
||||||
const featurePath = path.join(specsDir, featureFile)
|
const featurePath = path.join(specsDir, featureFile)
|
||||||
|
|
||||||
|
if (isUpToDate(featurePath, base, commit)) continue
|
||||||
|
|
||||||
const irPath = path.join(irDir, `${base}.json`)
|
const irPath = path.join(irDir, `${base}.json`)
|
||||||
|
|
||||||
sh('bb', ['gherkin-parser', featurePath, irPath], { cwd: apsDir })
|
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
|
const tests = fs
|
||||||
|
|||||||
@@ -23,12 +23,22 @@ function relativeImport (fromDir, targetFile) {
|
|||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hashFile (file) {
|
||||||
|
try {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function main () {
|
function main () {
|
||||||
const irArg = process.argv[2]
|
const irArg = process.argv[2]
|
||||||
const outArg = process.argv[3]
|
const outArg = process.argv[3]
|
||||||
|
const featureArg = process.argv[4]
|
||||||
|
const apsCommit = process.argv[5]
|
||||||
|
|
||||||
if (!irArg || !outArg) {
|
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)
|
process.exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +93,9 @@ main().catch((err) => { console.error(err); process.exit(1) })
|
|||||||
ir_path: path.resolve(irArg),
|
ir_path: path.resolve(irArg),
|
||||||
implementation_hash: `sha256:${hash}`,
|
implementation_hash: `sha256:${hash}`,
|
||||||
hash_scope: 'generated_files',
|
hash_scope: 'generated_files',
|
||||||
generated_files: [testFile]
|
generated_files: [testFile],
|
||||||
|
feature_hash: featureArg ? hashFile(featureArg) : null,
|
||||||
|
aps_commit: apsCommit || null
|
||||||
}
|
}
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(metaDir, metadataName(featureKey)),
|
path.join(metaDir, metadataName(featureKey)),
|
||||||
|
|||||||
Reference in New Issue
Block a user