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",
"env": {
"es2022": true
},
"rules": {
"array-callback-return": "off",
"arrow-parens": ["error", "always"],
......
/dist/
/dist/*
!/dist/.gitkeep
/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:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule' || $CI_PIPELINE_SOURCE == 'web'
......@@ -11,7 +14,7 @@ workflow:
- '{adapters,bin,harness,tests}'
variables:
GIT_DEPTH: '5'
DEFAULT_NODE_VERSION: '16'
DEFAULT_NODE_VERSION: '20'
LINUX_DISTRO: bullseye
NPM_CONFIG_AUDIT: 'false'
NPM_CONFIG_CACHE: &npm_cache_dir .cache/npm
......@@ -55,7 +58,7 @@ default:
paths:
- *npm_cache_dir
policy: pull
script: npm test
script: npm run test:ci
# this job signals success to the MR UI
docs:
stage: test
......@@ -72,13 +75,17 @@ lint:
script:
- npm run lint
- 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
<<: *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.]+) *[|]/'
<<: *save_report_artifacts
test:node-18-linux:
extends: .npm
image: node:18-$LINUX_DISTRO
<<: *unless_docs_mr
upload-artifacts:
tags:
- origin:eclipse
extends: .ef-download-eclipse.org
stage: deploy
'use strict'
module.exports = require('./harness/test/config/index.cjs')
export default {
input: 'harness/lib/index.js',
output: {
file: 'harness/lib/index.cjs',
format: 'cjs'
},
external: [
"node:child_process",
"node:fs/promises",
"node:url",
"node:path",
"glob",
"chai",
"mocha"
]
export default async () => {
return {
input: 'harness/lib/index.js',
output: {
intro: 'globalThis.NODE_SEA = "true";',
file: 'dist/index.cjs',
format: 'cjs'
},
external: [
'node:process',
'node:path',
'node:util',
'node:test',
'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
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.
== 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
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]
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
process.pkg
? require('../lib/index.cjs') // generated from index.js by Rollup
: import('../lib/index.js')
import('../lib/index.js')
/* global fetch */
import { exec } from 'node:child_process'
export default class AdapterManager {
constructor (adapter) {
this.adapter = adapter
class AdapterServerManager {
constructor ({ startCommand, stopCommand, url }) {
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) {
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)
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 {
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 resolveCliArgs from './cli.js'
import process from 'node:process'
import testSuite from './suites/suite.js'
const adapter = process.env.ASCIIDOC_TCK_ADAPTER
if (adapter) {
;(async () =>
run(adapter).then(
(result) => process.exit(result.failures),
(err) => console.error('Something went wrong!', err) || process.exit(1)
))()
} else {
console.error('No tests run.')
console.error('You must specify the path to a TCK adapter using the ASCIIDOC_TCK_ADAPTER environment variable.')
process.exit(1)
}
// rollup does not support top-level await when producing a cjs output
;(async () => {
const args = process.argv.slice(2)
if (globalThis.NODE_SEA === 'true' && args[0] === '//asciidoc-tck-test-suite.js') {
try {
await testSuite(process.env.ASCIIDOC_TCK_TESTS || './tests')
} catch (err) {
console.error('Something went wrong!', err)
process.exit(1)
}
} else {
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 loadTests from './test-loader.js'
import Mocha from './test-framework.js'
import process from 'node:process'
import { spec } from 'node:test/reporters'
import ospath from 'node:path'
import { run } from './test-framework.js'
import { toModuleContext } from './helpers.js'
const PACKAGE_DIR = ospath.join(toModuleContext(import.meta).__dirname, '../..')
export default async (adapter, options = {}) => {
const mocha = new Mocha({ timeout: 5000, ...options })
// TODO start and stop adapter in global hooks
const adapterManager = new AdapterManager(adapter)
Object.assign(mocha.suite.ctx, {
async parse (input) {
return adapterManager.post(input)
},
tests: await loadTests(ospath.join(PACKAGE_DIR, 'tests')),
})
mocha.files = [
ospath.join(PACKAGE_DIR, 'harness/lib/suites/block-test.js'),
ospath.join(PACKAGE_DIR, 'harness/lib/suites/inline-test.js'),
]
await mocha.loadFilesAsync()
return new Promise((resolve) => {
const runner = mocha.run((failures) => resolve({ failures, stats: runner.stats }))
const ac = new AbortController()
ac.signal.addEventListener(
'abort',
(event) => {
console.log('Tests aborted with event: ' + event.type)
},
{ once: true }
)
/**
* @param config
* @returns {Promise<{fail: number, pass: number}>}
*/
export default async (config) => {
const adapterConfig = config.adapter
// pass data to the tests runner as environment variables
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) {
/* eslint-env mocha */
for (const test of tests) {
const { name, type } = test
export function makeTests (testCases, testBlock) {
for (const testCase of testCases) {
const { name, type } = testCase
if (type === 'dir') {
describe(name, () => makeTests(test.entries, testBlock))
describe(name, () => makeTests(testCase.entries, testBlock))
} else {
;(it[test.condition] || it)(name, function () {
return testBlock.call(this, test.data)
;(it[testCase.condition] || it)(name, function () {
return testBlock.call(this, testCase.data)
})
}
}
......@@ -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