Skip to content

Commit

Permalink
Break up giant content linter test suite (#42144)
Browse files Browse the repository at this point in the history
  • Loading branch information
rachmari authored Sep 8, 2023
1 parent bdf9be5 commit 31d83ed
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 282 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@ jobs:
{ name: 'github-apps', path: 'src/github-apps/tests', },
{ name: 'graphql', path: 'src/graphql/tests', },
{ name: 'landings', path: 'src/landings/tests', },
// { name: 'learning-track', path: 'src/learning-track/tests', },
{ name: 'learning-track', path: 'src/learning-track/tests', },
{ name: 'linting', path: 'src/content-linter/tests', },
{ name: 'observability', path: 'src/observability/tests' },
{ name: 'pageinfo', path: 'src/pageinfo/tests', },
{ name: 'redirects', path: 'src/redirects/tests', },
{ name: 'release-notes', path: 'src/release-notes/tests', },
{ name: 'rendering', path: 'tests/rendering', },
{ name: 'rendering-fixtures', path: 'tests/rendering-fixtures', },
{ name: 'rest', path: 'src/rest/tests', },
{ name: 'routing', path: 'tests/routing', },
{ name: 'search', path: 'src/search/tests', },
{ name: 'secret-scanning', path: 'src/secret-scanning/tests',},
{ name: 'shielding', path: 'src/shielding/tests', },
context.payload.repository.full_name === 'github/docs-internal' &&
{ name: 'languages', path: 'src/languages/tests', },
Expand Down
16 changes: 16 additions & 0 deletions lib/ajv-validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Ajv from 'ajv'
import addErrors from 'ajv-errors'
import addFormats from 'ajv-formats'
import semver from 'semver'

const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
addFormats(ajv)
addErrors(ajv)
// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. ***
ajv.addFormat('semver', {
validate: (x) => semver.validRange(x),
})

export function ajvValidate(schema) {
return ajv.compile(schema)
}
18 changes: 0 additions & 18 deletions src/content-linter/tests/lint-code-languages.js

This file was deleted.

224 changes: 2 additions & 222 deletions src/content-linter/tests/lint-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,16 @@ import slash from 'slash'
import walk from 'walk-sync'
import { zip } from 'lodash-es'
import yaml from 'js-yaml'
import Ajv from 'ajv'
import addErrors from 'ajv-errors'
import addFormats from 'ajv-formats'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { visit } from 'unist-util-visit'
import fs from 'fs/promises'
import { existsSync } from 'fs'
import semver from 'semver'
import { jest } from '@jest/globals'

import { frontmatter, deprecatedProperties } from '../../../lib/frontmatter.js'
import languages from '#src/languages/lib/languages.js'
import releaseNotesSchema from '../lib/release-notes-schema.js'
import learningTracksSchema from '../lib/learning-tracks-schema.js'
import { liquid } from '#src/content-render/index.js'
import { getDiffFiles } from '../lib/diff-files.js'
import { formatAjvErrors } from '../../../tests/helpers/schemas.js'

jest.useFakeTimers({ legacyFakeTimers: true })

Expand All @@ -31,9 +24,6 @@ const contentDir = path.join(rootDir, 'content')
const reusablesDir = path.join(rootDir, 'data/reusables')
const variablesDir = path.join(rootDir, 'data/variables')
const glossariesDir = path.join(rootDir, 'data/glossaries')
const ghesReleaseNotesDir = path.join(rootDir, 'data/release-notes/enterprise-server')
const ghaeReleaseNotesDir = path.join(rootDir, 'data/release-notes/github-ae')
const learningTracks = path.join(rootDir, 'data/learning-tracks')
const fbvDir = path.join(rootDir, 'data/features')

const languageCodes = Object.keys(languages)
Expand Down Expand Up @@ -217,7 +207,7 @@ const yamlWalkOptions = {
}

// different lint rules apply to different content types
let mdToLint, ymlToLint, ghesReleaseNotesToLint, ghaeReleaseNotesToLint, learningTracksToLint
let mdToLint, ymlToLint

// compile lists of all the files we want to lint

Expand Down Expand Up @@ -294,35 +284,11 @@ const FbvYamlAbsPaths = walk(fbvDir, yamlWalkOptions).sort()
const FbvYamlRelPaths = FbvYamlAbsPaths.map((p) => slash(path.relative(rootDir, p)))
const fbvTuples = zip(FbvYamlRelPaths, FbvYamlAbsPaths)

// GHES release notes
const ghesReleaseNotesYamlAbsPaths = walk(ghesReleaseNotesDir, yamlWalkOptions).sort()
const ghesReleaseNotesYamlRelPaths = ghesReleaseNotesYamlAbsPaths.map((p) =>
slash(path.relative(rootDir, p)),
)
ghesReleaseNotesToLint = zip(ghesReleaseNotesYamlRelPaths, ghesReleaseNotesYamlAbsPaths)

// GHAE release notes
const ghaeReleaseNotesYamlAbsPaths = walk(ghaeReleaseNotesDir, yamlWalkOptions).sort()
const ghaeReleaseNotesYamlRelPaths = ghaeReleaseNotesYamlAbsPaths.map((p) =>
slash(path.relative(rootDir, p)),
)
ghaeReleaseNotesToLint = zip(ghaeReleaseNotesYamlRelPaths, ghaeReleaseNotesYamlAbsPaths)

// Learning tracks
const learningTracksYamlAbsPaths = walk(learningTracks, yamlWalkOptions).sort()
const learningTracksYamlRelPaths = learningTracksYamlAbsPaths.map((p) =>
slash(path.relative(rootDir, p)),
)
learningTracksToLint = zip(learningTracksYamlRelPaths, learningTracksYamlAbsPaths)

// Put all the yaml files together
ymlToLint = [].concat(
variableYamlTuples, // These "tuples" not tested independently; they are only tested as part of ymlToLint.
glossariesYamlTuples,
fbvTuples,
ghesReleaseNotesToLint,
ghaeReleaseNotesToLint,
learningTracksToLint,
)

function formatLinkError(message, links) {
Expand Down Expand Up @@ -361,19 +327,9 @@ if (diffFiles.length > 0) {
)
mdToLint = filterFiles(mdToLint)
ymlToLint = filterFiles(ymlToLint)
ghesReleaseNotesToLint = filterFiles(ghesReleaseNotesToLint)
ghaeReleaseNotesToLint = filterFiles(ghaeReleaseNotesToLint)
learningTracksToLint = filterFiles(learningTracksToLint)
}

if (
mdToLint.length +
ymlToLint.length +
ghesReleaseNotesToLint.length +
ghaeReleaseNotesToLint.length +
learningTracksToLint.length <
1
) {
if (mdToLint.length + ymlToLint.length < 1) {
// With this in place, at least one `test()` is called and you don't
// get the `Your test suite must contain at least one test.` error
// from `jest`.
Expand All @@ -382,18 +338,6 @@ if (
})
}

// ajv for schema validation tests
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true })
addFormats(ajv)
addErrors(ajv)
// *** TODO: We can drop this override once the frontmatter schema has been updated to work with AJV. ***
ajv.addFormat('semver', {
validate: (x) => semver.validRange(x),
})
// *** End TODO ***
const ghesValidate = ajv.compile(releaseNotesSchema)
const learningTracksValidate = ajv.compile(learningTracksSchema)

describe('lint markdown content', () => {
if (mdToLint.length < 1) return

Expand Down Expand Up @@ -881,167 +825,3 @@ describe('lint yaml content', () => {
})
})
})

describe('lint GHES release notes', () => {
if (ghesReleaseNotesToLint.length < 1) return
describe.each(ghesReleaseNotesToLint)('%s', (yamlRelPath, yamlAbsPath) => {
let dictionary
let dictionaryError = false

beforeAll(async () => {
const fileContents = await fs.readFile(yamlAbsPath, 'utf8')
try {
dictionary = yaml.load(fileContents, { filename: yamlRelPath })
} catch (error) {
dictionaryError = error
}
})

it('can be parsed as a single yaml document', () => {
expect(dictionaryError).toBe(false)
})

it('matches the schema', () => {
const valid = ghesValidate(dictionary)
let errors

if (!valid) {
errors = formatAjvErrors(ghesValidate.errors)
}

expect(valid, errors).toBe(true)
})

it('contains valid liquid', () => {
const { intro, sections } = dictionary
let toLint = { intro }
for (const key in sections) {
const section = sections[key]
const label = `sections.${key}`
section.forEach((part) => {
if (Array.isArray(part)) {
toLint = { ...toLint, ...{ [label]: section.join('\n') } }
} else {
for (const prop in section) {
toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } }
}
}
})
}

for (const key in toLint) {
if (!toLint[key]) continue
expect(() => liquid.parse(toLint[key]), `${key} contains invalid liquid`).not.toThrow()
}
})
})
})

describe('lint GHAE release notes', () => {
if (ghaeReleaseNotesToLint.length < 1) return
const currentWeeksFound = []
describe.each(ghaeReleaseNotesToLint)('%s', (yamlRelPath, yamlAbsPath) => {
let dictionary
let dictionaryError = false

beforeAll(async () => {
const fileContents = await fs.readFile(yamlAbsPath, 'utf8')
try {
dictionary = yaml.load(fileContents, { filename: yamlRelPath })
} catch (error) {
dictionaryError = error
}
})

it('can be parsed as a single yaml document', () => {
expect(dictionaryError).toBe(false)
})

it('matches the schema', () => {
const valid = ghesValidate(dictionary)
let errors

if (!valid) {
errors = formatAjvErrors(ghesValidate.errors)
}

expect(valid, errors).toBe(true)
})

it('does not have more than one yaml file with currentWeek set to true', () => {
if (dictionary.currentWeek) currentWeeksFound.push(yamlRelPath)
const errorMessage = `Found more than one file with currentWeek set to true: ${currentWeeksFound.join(
'\n',
)}`
expect(currentWeeksFound.length, errorMessage).not.toBeGreaterThan(1)
})

it('contains valid liquid', () => {
const { intro, sections } = dictionary
let toLint = { intro }
for (const key in sections) {
const section = sections[key]
const label = `sections.${key}`
section.forEach((part) => {
if (Array.isArray(part)) {
toLint = { ...toLint, ...{ [label]: section.join('\n') } }
} else {
for (const prop in section) {
toLint = { ...toLint, ...{ [`${label}.${prop}`]: section[prop] } }
}
}
})
}

for (const key in toLint) {
if (!toLint[key]) continue
expect(() => liquid.parse(toLint[key]), `${key} contains invalid liquid`).not.toThrow()
}
})
})
})

describe('lint learning tracks', () => {
if (learningTracksToLint.length < 1) return

describe.each(learningTracksToLint)('%s', (yamlRelPath, yamlAbsPath) => {
let dictionary
let dictionaryError = false

beforeAll(async () => {
const fileContents = await fs.readFile(yamlAbsPath, 'utf8')
try {
dictionary = yaml.load(fileContents, { filename: yamlRelPath })
} catch (error) {
dictionaryError = error
}
})

it('can be parsed as a single yaml document', () => {
expect(dictionaryError).toBe(false)
})

it('matches the schema', () => {
const valid = learningTracksValidate(dictionary)
let errors

if (!valid) {
errors = formatAjvErrors(learningTracksValidate.errors)
}

expect(valid, errors).toBe(true)
})

it('contains valid liquid', () => {
const toLint = []
Object.values(dictionary).forEach(({ title, description }) => {
toLint.push(title)
toLint.push(description)
})

toLint.forEach((element) => {
expect(() => liquid.parse(element), `${element} contains invalid liquid`).not.toThrow()
})
})
})
})
41 changes: 0 additions & 41 deletions src/content-linter/tests/lint-secret-scanning-data.js

This file was deleted.

Loading

0 comments on commit 31d83ed

Please sign in to comment.