Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • eclipse/asciidoc-lang/asciidoc-tck
  • mojavelinux/asciidoc-tck
  • ggrossetie/asciidoc-tck
  • swhitesal/asciidoc-tck
  • nmlopes/asciidoc-tck
  • jaredh159/asciidoc-tck
6 results
Show changes
Commits on Source (16)
Showing
with 538 additions and 182 deletions
{ {
"extends": "standard", "extends": "standard",
"env": {
"es2022": true
},
"rules": { "rules": {
"array-callback-return": "off", "array-callback-return": "off",
"arrow-parens": ["error", "always"], "arrow-parens": ["error", "always"],
......
/dist/ /dist/*
!/dist/.gitkeep
/node_modules/ /node_modules/
/harness/lib/index.cjs /reports/*
/reports/ !/reports/.gitkeep
include:
- project: 'eclipsefdn/it/releng/gitlab-runner-service/gitlab-ci-templates'
file: '/jobs/download.eclipse.org.gitlab-ci.yml'
workflow: workflow:
rules: rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule' || $CI_PIPELINE_SOURCE == 'web' - if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule' || $CI_PIPELINE_SOURCE == 'web'
...@@ -11,7 +14,7 @@ workflow: ...@@ -11,7 +14,7 @@ workflow:
- '{adapters,bin,harness,tests}' - '{adapters,bin,harness,tests}'
variables: variables:
GIT_DEPTH: '5' GIT_DEPTH: '5'
DEFAULT_NODE_VERSION: '16' DEFAULT_NODE_VERSION: '20'
LINUX_DISTRO: bullseye LINUX_DISTRO: bullseye
NPM_CONFIG_AUDIT: 'false' NPM_CONFIG_AUDIT: 'false'
NPM_CONFIG_CACHE: &npm_cache_dir .cache/npm NPM_CONFIG_CACHE: &npm_cache_dir .cache/npm
...@@ -55,7 +58,7 @@ default: ...@@ -55,7 +58,7 @@ default:
paths: paths:
- *npm_cache_dir - *npm_cache_dir
policy: pull policy: pull
script: npm test script: npm run test:ci
# this job signals success to the MR UI # this job signals success to the MR UI
docs: docs:
stage: test stage: test
...@@ -72,13 +75,17 @@ lint: ...@@ -72,13 +75,17 @@ lint:
script: script:
- npm run lint - npm run lint
- if [ -n "$(npm --silent run format && git --no-pager diff --name-only)" ]; then git --no-pager diff && false; fi - if [ -n "$(npm --silent run format && git --no-pager diff --name-only)" ]; then git --no-pager diff && false; fi
test:node-16-linux: test:node-20-linux:
extends: .npm extends: .npm
<<: *unless_docs_mr <<: *unless_docs_mr
script: npm run coverage script:
- npm run coverage
- npm run dist
- ./dist/asciidoc-tck cli -c 'node ./harness/test/sample-adapters/echo/index.js'
coverage: '/^All files *[|] *([0-9.]+) *[|]/' coverage: '/^All files *[|] *([0-9.]+) *[|]/'
<<: *save_report_artifacts <<: *save_report_artifacts
test:node-18-linux: upload-artifacts:
extends: .npm tags:
image: node:18-$LINUX_DISTRO - origin:eclipse
<<: *unless_docs_mr extends: .ef-download-eclipse.org
stage: deploy
'use strict'
module.exports = require('./harness/test/config/index.cjs')
export default { export default async () => {
input: 'harness/lib/index.js', return {
output: { input: 'harness/lib/index.js',
file: 'harness/lib/index.cjs', output: {
format: 'cjs' intro: 'globalThis.NODE_SEA = "true";',
}, file: 'dist/index.cjs',
external: [ format: 'cjs'
"node:child_process", },
"node:fs/promises", external: [
"node:url", 'node:process',
"node:path", 'node:path',
"glob", 'node:util',
"chai", 'node:test',
"mocha" 'node:assert',
] 'node:url',
'node:child_process',
'node:fs/promises',
'node:test/reporters'
]
}
} }
...@@ -79,13 +79,28 @@ The TCK will take over from there and run all the tests by hooking in the specif ...@@ -79,13 +79,28 @@ The TCK will take over from there and run all the tests by hooking in the specif
In order to pass the AsciiDoc TCK tests, you must create a CLI interface that reads AsciiDoc content from stdin and return an Abstract Semantic Graph (encoded in JSON) to stdout. In order to pass the AsciiDoc TCK tests, you must create a CLI interface that reads AsciiDoc content from stdin and return an Abstract Semantic Graph (encoded in JSON) to stdout.
The CLI must return an exit code of 0 on success and non-zero in case of failure. The CLI must return an exit code of 0 on success and non-zero in case of failure.
== Install == Input format
Download the appropriate binary from the release page. [source,json]
....
{
"contents": "...",
"path": "/a/path/to/the/input/file",
"type": "block" # "block" or "inline"
}
....
== Usage == Usage
Open a terminal and type: Releases are not available yet, but you can build from source by checking out the repo and running:
[source,sh] [source,sh]
ASCIIDOC_TCK_ADAPTER=/path/to/impl asciidoc-tck-linux-x64 npm ci
npm run dist
Then open a terminal and type:
[source,sh]
node harness/bin/asciidoc-tck.js cli --adapter-command /path/to/impl
NOTE: Requires `node` >= 20
#!/usr/bin/env node #!/usr/bin/env node
process.pkg import('../lib/index.js')
? require('../lib/index.cjs') // generated from index.js by Rollup
: import('../lib/index.js')
/* global fetch */
import { exec } from 'node:child_process' import { exec } from 'node:child_process'
export default class AdapterManager { class AdapterServerManager {
constructor (adapter) { constructor ({ startCommand, stopCommand, url }) {
this.adapter = adapter this.startCommand = startCommand
this.stopCommand = stopCommand
this.url = url
}
async post (data) {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
return response.json()
}
async start () {
if (this.startCommand !== undefined) {
const url = await new Promise((resolve, reject) => {
exec(this.startCommand, (err, stdout, _stderr) => {
if (err) return reject(err)
resolve(stdout)
})
})
if (this.url === undefined) {
this.url = url
}
}
}
stop () {
if (this.stopCommand !== undefined) {
return new Promise((resolve, reject) => {
exec(this.stopCommand, (err, stdout, _stderr) => {
if (err) return reject(err)
resolve(stdout)
})
})
}
}
}
class AdapterCliManager {
constructor ({ command }) {
this.command = command
} }
post (data) { post (data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cp = exec(this.adapter, (err, stdout, _stderr) => { const cp = exec(this.command, (err, stdout, _stderr) => {
if (err) return reject(err) if (err) return reject(err)
resolve(JSON.parse(stdout)) try {
resolve(JSON.parse(stdout))
} catch (err) {
reject(err)
}
}) })
cp.stdin.on('error', (err) => undefined).end(data) // eslint-disable-line node/handle-callback-err cp.stdin.on('error', (_err) => undefined).end(JSON.stringify(data)) // eslint-disable-line n/handle-callback-err
}) })
} }
...@@ -19,3 +68,32 @@ export default class AdapterManager { ...@@ -19,3 +68,32 @@ export default class AdapterManager {
stop () {} stop () {}
} }
export function createAdapterManager () {
const adapterConfig = getAdapterConfig()
if (adapterConfig.server) {
return new AdapterServerManager(adapterConfig.server)
}
return new AdapterCliManager(adapterConfig.cli)
}
function getAdapterConfig () {
if (process.env.ASCIIDOC_TCK_ADAPTER_MODE === 'server') {
const startCommand = process.env.ASCIIDOC_TCK_ADAPTER_SERVER_START_COMMAND
const stopCommand = process.env.ASCIIDOC_TCK_ADAPTER_SERVER_STOP_COMMAND
const url = process.env.ASCIIDOC_TCK_ADAPTER_URL
return {
server: {
...(url && { url }),
...(startCommand && { startCommand }),
...(stopCommand && { stopCommand }),
},
}
}
const command = process.env.ASCIIDOC_TCK_ADAPTER_CLI_COMMAND
return {
cli: {
...(command && { command }),
},
}
}
import { parseArgs } from 'node:util'
import process from 'node:process'
export default function resolveCliArgs (args = process.argv.slice(2)) {
const { values, positionals } = parseArgs({
args,
allowPositionals: true,
options: {
'start-command': {
type: 'string',
},
'stop-command': {
type: 'string',
},
url: {
type: 'string',
short: 'u',
},
'adapter-command': {
type: 'string',
short: 'c',
},
tests: {
type: 'string',
},
},
})
if (positionals.length === 0) {
throw new Error('You must specify the TCK adapter mode using either "cli" or "server" as a positional argument')
}
const testsDir = values.tests
const config = testsDir ? { testsDir } : {}
const startCommand = values['start-command']
const stopCommand = values['stop-command']
if (positionals.length === 1) {
const adapterMode = positionals[0]
if (adapterMode === 'cli') {
if (values['adapter-command'] === undefined) {
throw new Error('You must specify the TCK adapter command using --adapter-command or -c')
}
return {
...config,
adapter: {
cli: {
command: values['adapter-command'],
},
},
}
}
if (adapterMode === 'server') {
const url = values.url
if (url === undefined && startCommand === undefined) {
throw new Error(
'You must specify either specify a start command using --start-command or an URL using --url or -u'
)
}
return {
...config,
adapter: {
server: {
...(url && { url }),
...(startCommand && { startCommand }),
...(stopCommand && { stopCommand }),
},
},
}
}
// assumes that type is CLI and that the positional argument is the command
return {
...config,
adapter: {
cli: {
command: positionals[0],
},
},
}
}
if (positionals.length === 2) {
const adapterMode = positionals[0]
if (adapterMode === 'cli') {
return {
...config,
adapter: {
cli: {
command: positionals[1],
},
},
}
}
if (adapterMode === 'server') {
return {
...config,
adapter: {
server: {
url: positionals[1],
...(startCommand && { startCommand }),
...(stopCommand && { stopCommand }),
},
},
}
}
throw new Error('The TCK adapter mode must be either "cli" or "server"')
}
}
import run from './runner.js' import run from './runner.js'
import resolveCliArgs from './cli.js'
import process from 'node:process'
import testSuite from './suites/suite.js'
const adapter = process.env.ASCIIDOC_TCK_ADAPTER // rollup does not support top-level await when producing a cjs output
;(async () => {
if (adapter) { const args = process.argv.slice(2)
;(async () => if (globalThis.NODE_SEA === 'true' && args[0] === '//asciidoc-tck-test-suite.js') {
run(adapter).then( try {
(result) => process.exit(result.failures), await testSuite(process.env.ASCIIDOC_TCK_TESTS || './tests')
(err) => console.error('Something went wrong!', err) || process.exit(1) } catch (err) {
))() console.error('Something went wrong!', err)
} else { process.exit(1)
console.error('No tests run.') }
console.error('You must specify the path to a TCK adapter using the ASCIIDOC_TCK_ADAPTER environment variable.') } else {
process.exit(1) let config
} try {
config = resolveCliArgs()
} catch (err) {
console.error('No tests run.')
console.error(err.message)
process.exit(1)
}
try {
const { fail } = await run(config)
process.exit(fail)
} catch (err) {
console.error('Something went wrong!', err)
process.exit(1)
}
}
})()
import AdapterManager from './adapter-manager.js' import process from 'node:process'
import loadTests from './test-loader.js' import { spec } from 'node:test/reporters'
import Mocha from './test-framework.js'
import ospath from 'node:path' import ospath from 'node:path'
import { run } from './test-framework.js'
import { toModuleContext } from './helpers.js' import { toModuleContext } from './helpers.js'
const PACKAGE_DIR = ospath.join(toModuleContext(import.meta).__dirname, '../..') const PACKAGE_DIR = ospath.join(toModuleContext(import.meta).__dirname, '../..')
export default async (adapter, options = {}) => { const ac = new AbortController()
const mocha = new Mocha({ timeout: 5000, ...options }) ac.signal.addEventListener(
// TODO start and stop adapter in global hooks 'abort',
const adapterManager = new AdapterManager(adapter) (event) => {
Object.assign(mocha.suite.ctx, { console.log('Tests aborted with event: ' + event.type)
async parse (input) { },
return adapterManager.post(input) { once: true }
}, )
tests: await loadTests(ospath.join(PACKAGE_DIR, 'tests')),
}) /**
mocha.files = [ * @param config
ospath.join(PACKAGE_DIR, 'harness/lib/suites/block-test.js'), * @returns {Promise<{fail: number, pass: number}>}
ospath.join(PACKAGE_DIR, 'harness/lib/suites/inline-test.js'), */
] export default async (config) => {
await mocha.loadFilesAsync() const adapterConfig = config.adapter
return new Promise((resolve) => { // pass data to the tests runner as environment variables
const runner = mocha.run((failures) => resolve({ failures, stats: runner.stats })) if (config.testsDir !== undefined) {
process.env.ASCIIDOC_TCK_TESTS = config.testsDir
}
if (adapterConfig.cli) {
process.env.ASCIIDOC_TCK_ADAPTER_MODE = 'cli'
process.env.ASCIIDOC_TCK_ADAPTER_CLI_COMMAND = adapterConfig.cli.command
} else if (adapterConfig.server) {
process.env.ASCIIDOC_TCK_ADAPTER_MODE = 'server'
process.env.ASCIIDOC_TCK_ADAPTER_SERVER_START_COMMAND = adapterConfig.server.startCommand
process.env.ASCIIDOC_TCK_ADAPTER_SERVER_STOP_COMMAND = adapterConfig.server.stopCommand
process.env.ASCIIDOC_TCK_ADAPTER_URL = adapterConfig.server.url
}
const testFile =
globalThis.NODE_SEA === 'true'
? '//asciidoc-tck-test-suite.js' // virtual file
: ospath.join(PACKAGE_DIR, 'harness/lib/suites/index.js')
const testsStream = run({
files: [testFile],
signal: ac.signal,
}) })
let fail = 0
let pass = 0
const errors = []
let reporter
if (config.reporter === false) {
reporter = () => {}
} else {
const specReporter = spec()
reporter = (event) =>
specReporter._transform(event, 'utf-8', (_, data) => {
if (data) {
process.stdout.write(data)
}
})
}
for await (const event of testsStream) {
reporter(event)
if (event.type === 'error') {
throw event.data
}
if (event.type === 'test:pass' && event.data.details.type !== 'suite') {
pass += 1
}
if (event.type === 'test:fail' && event.data.details.type !== 'suite') {
fail += 1
errors.push(event.data.details.error.message)
}
}
return {
fail,
pass,
errors,
}
} }
/* eslint-env mocha */
import { expect, makeTests, populateASGDefaults, stringifyASG } from '../test-framework.js'
describe('block', function () {
const { parse, tests } = this.parent.ctx
const blockTests = tests.find((it) => it.type === 'dir' && it.name === 'block').entries
makeTests(blockTests, async function ({ input, inputPath, expected, expectedWithoutLocations }) {
const actual = await parse(input)
if (expected == null) {
// Q: can we write data to expect file automatically?
// TODO only output expected if environment variable is set
console.log(stringifyASG(actual))
this.skip()
} else {
const msg = `actual output does not match expected output for ${inputPath}`
// TODO wrap this in expectAsg helper
expect(actual, msg).to.eql(populateASGDefaults('location' in actual ? expected : expectedWithoutLocations))
}
})
})
import testsSuite from './suite.js'
;(async () => {
await testsSuite()
})()
/* eslint-env mocha */
import { expect, makeTests, stringifyASG } from '../test-framework.js'
describe('inline', function () {
const { parse, tests } = this.parent.ctx
const inlineTests = tests.find((it) => it.type === 'dir' && it.name === 'inline').entries
makeTests(inlineTests, async function ({ input, inputPath, expected, expectedWithoutLocations }) {
const actualRoot = await parse(input)
expect(actualRoot).to.have.property('blocks')
expect(actualRoot.blocks).to.have.lengthOf(1)
expect(actualRoot.blocks[0]).to.have.property('inlines')
const actual = actualRoot.blocks[0].inlines
if (expected == null) {
// Q: can we write data to expected file automatically?
// TODO only output expected if environment variable is set
console.log(stringifyASG(actual))
this.skip()
} else {
const msg = `actual output does not match expected output for ${inputPath}`
// TODO wrap this in expectAsg helper
expect(actual, msg).to.eql(!actual.length || 'location' in actual[0] ? expected : expectedWithoutLocations)
}
})
})
import ospath from 'node:path'
import { before, after, describe, makeTests, populateASGDefaults, assert, stringifyASG } from '../test-framework.js'
import { createAdapterManager } from '../adapter-manager.js'
import loadTests from '../test-loader.js'
import { toModuleContext } from '../helpers.js'
const PACKAGE_DIR = ospath.join(toModuleContext(import.meta).__dirname, '../../..')
const adapterManager = createAdapterManager()
async function parse (input) {
return adapterManager.post(input)
}
const testsSuite = async (testDir = ospath.join(PACKAGE_DIR, 'tests')) => {
const tests = await loadTests(testDir)
await describe('tests', () => {
before(async () => {
await adapterManager.start()
})
after(async () => {
await adapterManager.stop()
})
describe('inline', function () {
const inlineTests = tests.find((it) => it.type === 'dir' && it.name === 'inline').entries
makeTests(inlineTests, async function ({ input, inputPath, expected, expectedWithoutLocations }) {
const actual = await parse({
contents: input,
path: inputPath,
type: 'inline',
})
if (expected == null) {
// Q: can we write data to expected file automatically?
// TODO only output expected if environment variable is set
console.log(stringifyASG(actual))
this.skip()
} else {
const msg = `actual output does not match expected output for ${inputPath}`
// TODO wrap this in expectAsg helper
assert.deepEqual(actual, !actual.length || 'location' in actual[0] ? expected : expectedWithoutLocations, msg)
}
})
})
describe('block', function () {
const blockTests = tests.find((it) => it.type === 'dir' && it.name === 'block').entries
makeTests(blockTests, async function ({ input, inputPath, expected, expectedWithoutLocations }) {
const actual = await parse({
contents: input,
path: inputPath,
type: 'block',
})
if (expected == null) {
// Q: can we write data to expect file automatically?
// TODO only output expected if environment variable is set
console.log(stringifyASG(actual))
this.skip()
} else {
const msg = `actual output does not match expected output for ${inputPath}`
// TODO wrap this in expectAsg helper
assert.deepEqual(
populateASGDefaults(actual),
populateASGDefaults('location' in actual ? expected : expectedWithoutLocations),
msg
)
}
})
})
})
}
export default testsSuite
export { expect } from 'chai' import { describe, it } from 'node:test'
export function makeTests (tests, testBlock) { export function makeTests (testCases, testBlock) {
/* eslint-env mocha */ for (const testCase of testCases) {
for (const test of tests) { const { name, type } = testCase
const { name, type } = test
if (type === 'dir') { if (type === 'dir') {
describe(name, () => makeTests(test.entries, testBlock)) describe(name, () => makeTests(testCase.entries, testBlock))
} else { } else {
;(it[test.condition] || it)(name, function () { ;(it[testCase.condition] || it)(name, function () {
return testBlock.call(this, test.data) return testBlock.call(this, testCase.data)
}) })
} }
} }
...@@ -68,4 +67,5 @@ export function stringifyASG (asg) { ...@@ -68,4 +67,5 @@ export function stringifyASG (asg) {
}) })
} }
export { default } from 'mocha' export * from 'node:test'
export * as assert from 'node:assert'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import resolveCliArgs from '../lib/cli.js'
describe('resolveCliArgs()', () => {
it('should parse cli shorthand format', () => {
const opts = resolveCliArgs(['cli', '/path/to/cmd'])
assert.deepEqual(opts, {
adapter: {
cli: {
command: '/path/to/cmd',
},
},
})
})
it('should parse cli shorthand format using a single positional argument', () => {
const opts = resolveCliArgs(['/path/to/cmd'])
assert.deepEqual(opts, {
adapter: {
cli: {
command: '/path/to/cmd',
},
},
})
})
it('should parse server arguments with url', () => {
const opts = resolveCliArgs(['server', '-u', 'http://localhost:1234/asciidoc-tck'])
assert.deepEqual(opts, {
adapter: {
server: {
url: 'http://localhost:1234/asciidoc-tck',
},
},
})
})
it('should parse server arguments with url and stop command', () => {
const opts = resolveCliArgs([
'server',
'--url=http://localhost:1234/asciidoc-tck',
'--stop-command=/path/to/stop-server',
])
assert.deepEqual(opts, {
adapter: {
server: {
url: 'http://localhost:1234/asciidoc-tck',
stopCommand: '/path/to/stop-server',
},
},
})
})
it('should parse server arguments with start and stop commands', () => {
const opts = resolveCliArgs([
'server',
'--start-command=/path/to/start-server',
'--stop-command=/path/to/stop-server',
])
assert.deepEqual(opts, {
adapter: {
server: {
startCommand: '/path/to/start-server',
stopCommand: '/path/to/stop-server',
},
},
})
})
it('should throw an error when neither --url nor --start-command is defined', () => {
assert.throws(() => resolveCliArgs(['server']), {
message: 'You must specify either specify a start command using --start-command or an URL using --url or -u',
})
})
it('should throw an error when adapter mode is unrecognized', () => {
assert.throws(() => resolveCliArgs(['proxy', '/path/to/cmd']), {
message: 'The TCK adapter mode must be either "cli" or "server"',
})
})
it('should throw an error when argument is unrecognized', () => {
assert.throws(() => resolveCliArgs(['cli', '--unknown=foo']), {
message:
"Unknown option '--unknown'. To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- \"--unknown\"",
})
})
it('should throw an error when argument value is missing', () => {
assert.throws(() => resolveCliArgs(['cli', '--adapter-command']), {
message: "Option '-c, --adapter-command <value>' argument missing",
})
})
it('should throw an error when argument value is missing', () => {
assert.throws(() => resolveCliArgs(['cli', '-c']), {
message: "Option '-c, --adapter-command <value>' argument missing",
})
})
it('should parse command with tests-dir', () => {
const opts = resolveCliArgs(['cli', '--adapter-command=/path/to/cmd', '--tests=/path/to/tests'])
assert.deepEqual(opts, {
testsDir: '/path/to/tests',
adapter: {
cli: {
command: '/path/to/cmd',
},
},
})
})
})
'use strict'
const ospath = require('node:path')
const config = {
checkLeaks: true,
mochaGlobalTeardown () {
if (!this.failures) logCoverageReportPath()
},
require: __filename,
spec: resolveSpec(),
timeout: 10 * 60 * 1000,
}
if (process.env.npm_config_watch) config.watch = true
if (process.env.CI) {
Object.assign(config, {
forbidOnly: true,
reporter: ospath.join(__dirname, 'mocha-ci-reporter.cjs'),
'reporter-option': ['output=reports/tests-xunit.xml'],
})
}
function logCoverageReportPath () {
if (!process.env.CODE_COVERAGE) return
const { CI_PROJECT_PATH, CI_JOB_ID } = process.env
const coverageReportRelpath = 'reports/lcov-report/index.html'
const coverageReportURL = CI_JOB_ID
? `https://gitlab.com/${CI_PROJECT_PATH}/-/jobs/${CI_JOB_ID}/artifacts/file/${coverageReportRelpath}`
: require('url').pathToFileURL(coverageReportRelpath)
console.log(`Coverage report: ${coverageReportURL}`)
}
function resolveSpec () {
const spec = process.argv[2]
if (spec && !spec.startsWith('-')) return spec
return 'harness/test/**/*-test.js'
}
module.exports = config
'use strict'
const { Base, Dot, XUnit } = require('mocha').reporters
// A Mocha reporter that combines the dot and xunit reporters.
class CI extends Base {
constructor (runner, options) {
super(runner, options)
new Dot(runner, options) // eslint-disable-line no-new
new XUnit(runner, options) // eslint-disable-line no-new
}
}
module.exports = CI