Skip to content
Snippets Groups Projects
Commit d9b587b9 authored by Guillaume Grossetie's avatar Guillaume Grossetie :headphones:
Browse files

Linter, test, code coverage and GitLab CI

parent 35f9b122
No related branches found
No related tags found
1 merge request!1Linter, test, code coverage and GitLab CI
Pipeline #12691 waiting for manual action
{
"extends": "standard",
"rules": {
"array-callback-return": "off",
"arrow-parens": ["error", "always"],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline"
}],
"max-len": ["error", {
"code": 120,
"ignoreStrings": true,
"ignoreUrls": true,
"ignoreTemplateLiterals": true
}],
"prefer-regex-literals": "off",
"spaced-comment": "off",
"radix": ["error", "always"]
}
}
dist/
/dist/
node_modules/
lib/index.cjs
# generated
/lib/index.cjs
/coverage-report/
/reports/
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule' || $CI_PIPELINE_SOURCE == 'web'
# See https://docs.gitlab.com/ee/ci/yaml/workflow.html#switch-between-branch-pipelines-and-merge-request-pipelines
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS == null && $CI_COMMIT_BRANCH !~ /^docs\//
changes:
- .eslintrc
- .gitlab-ci.yml
- package{-lock,}.json
- '{fixtures,lib,test,bin}'
variables:
GIT_DEPTH: '5'
DEFAULT_NODE_VERSION: '16'
LINUX_DISTRO: bullseye
NPM_CONFIG_AUDIT: 'false'
NPM_CONFIG_CACHE: &npm_cache_dir .cache/npm
NPM_CONFIG_PREFER_OFFLINE: 'true'
default:
image: node:$DEFAULT_NODE_VERSION-$LINUX_DISTRO
interruptible: true
.defs:
- &if_docs_mr
rules:
- if: &docs_mr $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^docs\//
- &if_schedule_rule
rules:
- if: $CI_PIPELINE_SOURCE == 'schedule' || ($CI_PIPELINE_SOURCE == 'web')
- &unless_docs_mr
rules:
- if: *docs_mr
when: never
- if: $CI_JOB_NAME == 'lint' && $CI_PIPELINE_SOURCE == 'push' && $CI_PROJECT_PATH != 'antora/antora-lunr-extension'
when: manual
- when: on_success
- &platform_info node -p '`${os.type()} ${os.release()}\nNode.js ${process.version}`'
- &save_report_artifacts
artifacts:
when: always
paths:
- reports/lcov-report
reports:
coverage_report:
coverage_format: cobertura
path: reports/cobertura-coverage.xml
junit: reports/tests-xunit.xml
.npm:
stage: test
<<: *if_schedule_rule
before_script:
- *platform_info
- npm ci --audit --quiet
cache: &npm_cache
key: npm-cache
paths:
- *npm_cache_dir
policy: pull
script: npm test
# this job signals success to the MR UI
docs:
stage: test
<<: *if_docs_mr
script: echo 'we love docs!'
# this job also seeds the dependency cache
lint:
extends: .npm
stage: .pre
<<: *unless_docs_mr
cache:
<<: *npm_cache
policy: pull-push
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:
extends: .npm
<<: *unless_docs_mr
script: npm run coverage
coverage: '/^All files *[|] *([0-9.]+) *[|]/'
<<: *save_report_artifacts
test:node-18-linux:
extends: .npm
image: node:18-$LINUX_DISTRO
<<: *unless_docs_mr
'use strict'
module.exports = require('./test/config.cjs')
.npmrc 0 → 100644
fund=false
lockfile-version=3
omit=optional
......@@ -3,7 +3,7 @@
'use strict'
if (process.pkg) {
// index.cjs is generated from index.js by Rollup
// index.cjs is generated from index.js by Rollup
require('../lib/index.cjs')
} else {
import('../lib/index.js')
......
import { exec as execProcess } from 'node:child_process'
import fsp from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import ospath from 'node:path'
import glob from 'glob'
import chai from 'chai'
import Mocha from 'mocha'
'use strict'
//import yargs from 'yargs'
//import { hideBin } from 'yargs/helpers'
import runner from './runner.js'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
const argv = yargs(hideBin(process.argv))
.help()
.argv
console.log(argv)
const __dirname = fileURLToPath(new URL('.', import.meta.url))
//const argv = yargs(hideBin(process.argv)).help().argv
const adapter = process.env.ASCIIDOC_TCK_ADAPTER
async function getFiles() {
return new Promise((resolve, reject) => {
glob(ospath.join(__dirname, '..', 'fixtures', '**/*.input.adoc'), async function (err, files) {
if (err) {
reject(err)
} else {
resolve(files)
}
})
})
}
async function exec(content) {
return new Promise((resolve, reject) => {
const childProcess = execProcess(`${adapter}`, (error, stdout, stderr) => {
if (error) {
reject(error)
return
}
resolve({ stdout, stderr })
})
childProcess.on('error', (err) => {
console.log('ERROR', err)
})
write(childProcess.stdin, content, () => {
childProcess.stdin.end()
})
})
}
function write(stream, data, cb) {
if (!stream.write(data)) {
stream.once('drain', cb)
} else {
process.nextTick(cb)
}
}
if (adapter) {
;(async () => {
const files = await getFiles()
const mocha = new Mocha({})
const Test = Mocha.Test
const Suite = Mocha.Suite
const suites = {}
for (const file of files) {
const relative = ospath.relative(ospath.join(__dirname, '..', 'fixtures'), file)
const suiteName = ospath.dirname(relative)
let suite
if (suiteName in suites) {
suite = suites[suiteName]
} else {
suite = Suite.create(mocha.suite, suiteName)
suites[suiteName] = suite
}
const testName = ospath.basename(file).replace(/\.input\.adoc$/, '')
suite.addTest(new Test(testName, async () => {
const input = await fsp.readFile(file, 'utf8')
const expectedFile = ospath.join(__dirname, '..', 'fixtures', relative.replace(/\.input\.adoc$/, '.output.json'))
const expectedOutput = await fsp.readFile(expectedFile, 'utf8')
const childProcess = await exec(input)
const actualOutput = JSON.parse(childProcess.stdout)
chai.expect(actualOutput).to.deep.equal(JSON.parse(expectedOutput))
}))
try {
const result = await runner(adapter)
process.exit(result.failures)
} catch (e) {
console.error('Something wrong happened!', e)
}
mocha.run(() => {
process.exit(0)
})
})()
}
import { exec as execProcess } from 'node:child_process'
import fsp from 'node:fs/promises'
import ospath from 'node:path'
import { fileURLToPath } from 'node:url'
import Mocha from 'mocha'
import chai from 'chai'
import glob from 'glob'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
async function getFiles () {
return new Promise((resolve, reject) => {
glob(ospath.join(__dirname, '..', 'fixtures', '**/*.input.adoc'), async function (err, files) {
if (err) {
reject(err)
} else {
resolve(files)
}
})
})
}
async function exec (adapter, content) {
return new Promise((resolve, reject) => {
try {
const childProcess = execProcess(adapter, (error, stdout, stderr) => {
if (error) {
reject(error)
return
}
resolve({ stdout, stderr })
})
childProcess.stdin.on('error', (err) => {
reject(err)
})
write(childProcess.stdin, content, () => {
childProcess.stdin.end()
})
} catch (e) {
reject(e)
}
})
}
function write (stream, data, cb) {
if (!stream.write(data)) {
stream.once('drain', cb)
} else {
process.nextTick(cb)
}
}
export default async function (adapter, options) {
if (options === undefined) {
options = {}
}
const files = await getFiles()
const mocha = new Mocha(options)
const Test = Mocha.Test
const Suite = Mocha.Suite
const suites = {}
for (const file of files) {
const relative = ospath.relative(ospath.join(__dirname, '..', 'fixtures'), file)
const suiteName = ospath.dirname(relative)
let suite
if (suiteName in suites) {
suite = suites[suiteName]
} else {
suite = Suite.create(mocha.suite, suiteName)
suites[suiteName] = suite
}
const testName = ospath.basename(file).replace(/\.input\.adoc$/, '')
suite.addTest(
new Test(testName, async () => {
try {
const input = await fsp.readFile(file, 'utf8')
const expectedFile = ospath.join(
__dirname,
'..',
'fixtures',
relative.replace(/\.input\.adoc$/, '.output.json')
)
const expectedOutput = await fsp.readFile(expectedFile, 'utf8')
const childProcess = await exec(adapter, input)
const actualOutput = JSON.parse(childProcess.stdout)
chai.expect(actualOutput).to.deep.equal(JSON.parse(expectedOutput))
} catch (e) {
chai.expect.fail(e.message)
}
})
)
}
return new Promise((resolve) => {
const runner = mocha.run((result) => {
resolve({
failures: result,
stats: runner.stats,
})
})
})
}
'use strict'
import { promises as fsp } from 'node:fs'
import ospath from 'node:path'
import format from 'prettier-eslint'
async function formatAll (dirs, ignores, cwd = process.cwd()) {
const result = []
for (const dir of dirs) {
const subdirs = []
const absdir = ospath.join(cwd, dir)
for await (const dirent of await fsp.opendir(absdir)) {
const name = dirent.name
if (dirent.isDirectory()) {
if (name !== 'node_modules') subdirs.push(name)
} else if (name.endsWith('.js')) {
const filePath = ospath.join(absdir, name)
if (!~ignores.indexOf(filePath)) {
result.push(
await fsp.readFile(filePath, 'utf8').then((text) => {
const formatted = format({ text, filePath })
return formatted === text ? false : fsp.writeFile(filePath, formatted).then(() => true)
})
)
}
}
}
if (subdirs.length) result.push.apply(result, await formatAll(subdirs, ignores, absdir))
}
return result
}
;(async (dirlist) => {
const cwd = process.cwd()
const ignores = [] // reserved for future use
await formatAll(dirlist.split(','), ignores, cwd).then((result) => {
if (process.env.npm_config_loglevel === 'silent') return
const total = result.length
const changed = result.filter((it) => it).length
const unchanged = total - changed
const changedStatus = `changed ${changed} file${changed === 1 ? '' : 's'}`
const unchangedStatus = `left ${unchanged} file${unchanged === 1 ? '' : 's'} unchanged`
console.log(`prettier-eslint ${changedStatus} and ${unchangedStatus}`)
})
})(process.argv[2] || '')
This diff is collapsed.
......@@ -29,7 +29,15 @@
"outputPath": "dist"
},
"scripts": {
"build": "rollup -c && pkg ."
"dist": "rollup -c && pkg .",
"build": "npm run lint && npm t",
"coverage": "CODE_COVERAGE=1 c8 npm t",
"format": "node npm/format.js lib,test,npm,bin",
"lint": "eslint lib test npm bin --ext js",
"test": "_mocha",
"version": "node npm/version.js",
"prepublishOnly": "node npm/prepublishOnly.js",
"postpublish": "node npm/postpublish.js"
},
"keywords": [],
"author": "AsciiDoc Working Group",
......@@ -39,14 +47,35 @@
"homepage": "https://asciidoc.org",
"license": "EPL-2.0",
"dependencies": {
"chai": "^4.3.6",
"glob": "^8.0.3",
"mocha": "^10.0.0",
"yargs": "^17.6.0"
"chai": "~4.3",
"glob": "~8.0",
"mocha": "~10.0",
"yargs": "~17.6"
},
"devDependencies": {
"@asciidoctor/core": "2.2.6",
"c8": "~7.12",
"dirty-chai": "~2.0",
"eslint": "~7.32",
"eslint-config-standard": "~16.0",
"pkg": "5.8.0",
"prettier-eslint": "~13.0",
"rollup": "3.2.0"
}
},
"c8": {
"cache": true,
"cacheDir": "node_modules/.cache/nyc",
"include": [
"lib/*.js"
],
"reporter": [
"cobertura",
"lcov",
"text"
],
"reportDir": "reports"
},
"workspaces": [
"."
]
}
......@@ -2,7 +2,7 @@ import Asciidoctor from '@asciidoctor/core'
const asciidoctor = Asciidoctor()
function toDocumentObjectModel(doc, text) {
function toDocumentObjectModel (doc, text) {
const children = doc.getBlocks().map((block) => toNode(block))
return {
...(doc.hasHeader() && {
......@@ -14,7 +14,7 @@ function toDocumentObjectModel(doc, text) {
}
}
function toNode(block) {
function toNode (block) {
let type
if (block.getNodeName() === 'sidebar') {
type = 'Sidebar'
......@@ -35,7 +35,7 @@ function toNode(block) {
} else {
type = 'Block'
}
if (block['$content_model']() === 'simple') {
if (block.$content_model() === 'simple') {
const attributes = {}
const id = block.getId()
if (id) {
......@@ -65,12 +65,12 @@ function toNode(block) {
}
if (block.getNodeName() === 'listing' || block.getNodeName() === 'literal') {
const attributes = block.getAttributes()
let style = block.getStyle()
const style = block.getStyle()
if (style === block.getNodeName()) {
delete attributes.style
}
//const title = block.getTitle()
delete attributes['$positional']
delete attributes.$positional
return {
type,
...(Object.keys(attributes).length > 0 && { attributes }),
......@@ -109,7 +109,7 @@ function toNode(block) {
}
export default class AsciidoctorParser {
parse(text) {
parse (text) {
const doc = asciidoctor.load(text)
// fixme: transform doc into a generic DOM
const result = toDocumentObjectModel(doc, text)
......
......@@ -7,7 +7,8 @@ import { once } from 'node:events'
process.title = 'asciidoctor'
const readFromStdin = async () => new Promise((resolve, reject) => {
const readFromStdin = async () =>
new Promise((resolve, reject) => {
const encoding = 'utf-8'
let data
data = ''
......@@ -34,5 +35,3 @@ const readFromStdin = async () => new Promise((resolve, reject) => {
process.stdout.write(JSON.stringify(asciidoctorParser.parse(content)))
await once(process.stdout.end(), 'close')
})()
'use strict'
const config = {
mochaGlobalTeardown () {
if (!this.failures) logCoverageReportPath()
},
require: __filename,
timeout: 10 * 60 * 1000,
}
if (process.env.npm_config_watch) config.watch = true
if (process.env.CI) {
Object.assign(config, {
forbidOnly: true,
reporter: 'xunit',
'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}`)
}
module.exports = config
/* eslint-env mocha */
'use strict'
import chai from 'chai'
import runner from '../lib/runner.js'
describe('runner()', () => {
it('should fail when using an non existing adapter', async () => {
const result = await runner('non-existing-adapter', {
reporter: function () {
/* noop */
},
})
chai.expect(result.failures).to.eq(2)
chai.expect(result.stats.suites).to.eq(1)
chai.expect(result.stats.tests).to.eq(2)
chai.expect(result.stats.failures).to.eq(2)
})
})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment