Two more scenarios, and the recorder could not take either of them as it stood. - editors/getdata reads a table input, not URL parameters, so record.js can now post one: it writes the informational header the server expects (the first line is the SAS input statement, not a list of column names), posts it as multipart/form-data, and takes --libref so the estate's control library name stays out of scenarios.json. replay.js hands the mock its own rows through mockInput, since the two sides do not share a libref. - the test harness's webout() stub now honours the showmeta option and writes the body as members are added. Without them the mocks' own requireMemberMetadata() guard throws - it reads the body before CLOSE - and a service that stopped sending formats and labels would have passed here. - --scenario accepts service/id, because two screens legitimately call their first service 'default'. The two goldens are recorded but marked gated: false: the gate prints their difference counts and does not fail, and check.js labels them ADVISORY. They are a work list, not an acceptance. editors/getdata/mpe_tables - 388 differences, all of them the mock's: cols carries a DESC key SAS never emits and misses the LABEL/LONGDESC/COLTYPE SAS reads from MPE_DATADICTIONARY; sasdata leaves blank character values out instead of padding them and is not ordered by the business key; dqrules differ in content and order; approvers is hardcoded to one user where SAS computes it and returns none. public/viewlibs - 4 differences: SAS answers LIBRARYID='V9' with a blank ENGINE and LIBRARYNAME equal to the libref, and lists SASHELP; the mock answers LIBRARYID=<libref>, ENGINE='V9', LIBRARYNAME='Data Controller(<libref>)' and has no SASHELP. dsmeta is excluded on purpose: it is the engine's report on the dataset (owner, host, encoding, paths, created dates), estate state rather than behaviour, and the projection refused the recorded response until it was dropped - the site id and the service account name were in it.
83 lines
3.0 KiB
JavaScript
83 lines
3.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Replays every recorded golden against a mock estate and prints the
|
|
* differences.
|
|
*
|
|
* node sas/mocks/test/parity/check.js
|
|
* node sas/mocks/test/parity/check.js --scenario public/startupservice
|
|
* node sas/mocks/test/parity/check.js --advisory # report, exit 0
|
|
*
|
|
* This is the human-facing half of the same comparison `test/parity-check.test.js`
|
|
* runs as a gate; both go through `parity/replay.js`, so neither can drift from
|
|
* the other.
|
|
*
|
|
* Both sides are projections, so the values printed here are the placeholders
|
|
* the golden already carries - <LIBREF>, <PATH>, <STAGING_ID> - and a log of a
|
|
* failure is as safe to paste as the golden itself.
|
|
*/
|
|
// The mock derives SYSTIMEZONEOFFSET from the host, and the golden carries the
|
|
// estate's own offset: pin the zone so the accepted difference is the same on
|
|
// every machine, not only on the UTC container the suite usually runs in.
|
|
process.env.TZ = 'UTC'
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const { seededEstate, runService } = require('../harness')
|
|
const { replay } = require('./replay')
|
|
|
|
function arg(name, fallback) {
|
|
const index = process.argv.indexOf('--' + name)
|
|
if (index < 0) return fallback
|
|
const next = process.argv[index + 1]
|
|
return next && next.indexOf('--') !== 0 ? next : true
|
|
}
|
|
|
|
try {
|
|
const only = arg('scenario', null)
|
|
const advisory = arg('advisory', false) === true
|
|
const scenarios = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, 'scenarios.json'), 'utf8')
|
|
).scenarios.filter(function (scenario) {
|
|
return !only || scenario.id === only || scenario.service === only
|
|
})
|
|
if (!scenarios.length) throw new Error('no scenario matched ' + only)
|
|
|
|
// the golden is recorded against a stock estate (makedata only), so the mock
|
|
// side is built the same way - fixtures would add datasets the golden never saw
|
|
const estate = seededEstate({ demo: false })
|
|
let failed = 0
|
|
|
|
for (const result of replay(estate, runService, scenarios)) {
|
|
const id = result.scenario.service + '/' + result.scenario.id
|
|
if (!result.golden) {
|
|
console.log('SKIP ' + id + ' - no golden')
|
|
continue
|
|
}
|
|
const advisory = result.scenario.gated === false
|
|
if (result.unexpected.length || result.stale.length) {
|
|
console.log(
|
|
(advisory ? 'ADVISORY ' : 'DIFF ') + id + ' - ' +
|
|
result.unexpected.length + ' unaccepted, ' +
|
|
result.stale.length + ' stale acceptance(s)'
|
|
)
|
|
for (const difference of result.unexpected.slice(0, 40)) {
|
|
console.log(' ' + difference)
|
|
}
|
|
for (const stale of result.stale) {
|
|
console.log(' ' + stale + ': accepted, but no longer differs')
|
|
}
|
|
// a scenario marked gated: false is reported, never counted
|
|
if (!advisory) failed++
|
|
continue
|
|
}
|
|
console.log(
|
|
'ok ' + id + ' (' + result.accepted.length + ' accepted difference(s))'
|
|
)
|
|
}
|
|
|
|
if (failed && !advisory) process.exitCode = 1
|
|
} catch (err) {
|
|
console.error(err.message)
|
|
process.exitCode = 1
|
|
}
|