Skip to content

Commit

Permalink
chore: rfc-index.xml tests WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
holloway committed Jan 27, 2025
1 parent 64d4a7f commit 84c2845
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 163 deletions.
114 changes: 3 additions & 111 deletions client/utilities/rfc-index-txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { DateTime } from 'luxon'
import { padStart } from 'lodash-es'
import { SPACE } from './strings'
import type { ApiClient, RfcMetadata } from '~/generated/red-client'
import type { ExtraFieldsNeeded } from './rfc.mocks'
import { getRFCWithExtraFields } from './rfc.mocks'

// Note: this file is intentionally named rfc-index-txt.ts not rfc-index.txt.ts
// because vitest can't import that later filename
Expand Down Expand Up @@ -162,110 +164,6 @@ const stringifyAuthor = (author: RfcMetadata['authors'][number]): string => {
return author.affiliation === 'Editor' ? `${name}, Ed.` : name
}

type ExtraFieldNeededFormat =
| {
type: 'TXT'
}
| { type: 'HTML' }
| { type: 'HTMLized' }
| { type: 'PDF' }
| { type: 'PS' }

type ExtraFieldNeededObsolete = {
number: number
}

type ExtraFieldNeededUpdate = {
number: number
}

type ExtraFieldNeededIsAlso = {
number: number
}

type ExtraFieldNeededSeeAlso = {
number: number
}

type ExtraFieldsNeeded = {
formats: ExtraFieldNeededFormat[]
obsoletes: ExtraFieldNeededObsolete[]
updates: ExtraFieldNeededUpdate[]
is_also: ExtraFieldNeededIsAlso[]
see_also: ExtraFieldNeededSeeAlso[]
}

const missingData: Record<number, Partial<RfcMetadata & ExtraFieldsNeeded>> = {
1: {
authors: [
{
name: 'Steve Crocker',
person: 0
}
],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
},
2: {
authors: [{ name: 'Bill Duvall', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'PDF' }, { type: 'HTML' }]
},
3: {
authors: [{ name: 'Steve D. Crocker', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
},
4: {
authors: [{ name: 'E. B. Shapiro', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
},
5: {
authors: [{ name: 'J. Rulifson', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
},
6: {
authors: [{ name: 'S.D. Crocker.', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
},
7: {
authors: [{ name: 'G. Deloche', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
},
8: {
authors: [{ name: 'G. Deloche', person: 0 }],
formats: [{ type: 'PDF' }]
},
9: {
authors: [{ name: 'G. Deloche', person: 0 }],
formats: [{ type: 'PDF' }]
},
10: {
authors: [{ name: 'S.D. Crocker.', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }],
obsoletes: [{ number: 3 }],
updated_by: [
{ id: 24, number: 24, title: '?' },
{ id: 27, number: 27, title: '?' },
{ id: 30, number: 30, title: '?' }
]
},
11: {
authors: [{ name: 'G. Deloche', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'PDF' }, { type: 'HTML' }]
},
12: {
authors: [{ name: 'M. Wingfield', person: 0 }],
formats: [
{ type: 'TXT' },
{ type: 'PS' },
{ type: 'PDF' },
{ type: 'HTML' }
]
},
13: {
authors: [{ name: 'Vincent Cerf', person: 0 }],
formats: [{ type: 'TXT' }, { type: 'HTML' }]
}
}

const formatRfcNumber = (number: number): string => {
return `RFC${number.toString()}`
}
Expand All @@ -284,13 +182,7 @@ const stringifyRFC = (
let also = ''
let doi = ''

const missingRfc = missingData[rfcMetadata.number]

const rfc: RfcMetadata & Partial<ExtraFieldsNeeded> = {
...missingRfc,
...rfcMetadata,
authors: [...toArray(missingRfc?.authors), ...toArray(rfcMetadata?.authors)]
}
const rfc = getRFCWithExtraFields(rfcMetadata)

if (rfc.title === 'Not Issued') {
return 'Not Issued.'
Expand Down
36 changes: 19 additions & 17 deletions client/utilities/rfc-index-xml.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DateTime } from 'luxon'
import { XMLBuilder } from 'fast-xml-parser'
import type { ApiClient } from '~/generated/red-client'
import { getRFCWithExtraFields } from './rfc.mocks'

type DocListArg = Parameters<ApiClient['red']['docList']>[0]

Expand Down Expand Up @@ -78,34 +79,36 @@ const renderRFCs = async ({
const response = await redApi.red.docList(docListArg)

response.results.forEach((rfcMetadata) => {
const rfc = getRFCWithExtraFields(rfcMetadata)

const [month, year] = DateTime.fromISO(rfcMetadata.published)
.toFormat('LLLL yyyy')
.split(' ')

const abstractParagraphs = (rfcMetadata.abstract ?? '').split('\n')
const abstractParagraphs = (rfc.abstract ?? '').split('\n')

// FIXME: replace with RFC formats when the API has them
const formats = ['HTML', 'TEXT', 'PDF', 'XML']

const rfc = {
const rfcForXml = {
'rfc-entry': {
'doc-id': `RFC${rfcMetadata.number}`,
title: rfcMetadata.title,
...(rfcMetadata.authors.length > 0 ?
'doc-id': `rfc${rfc.number}`,
title: rfc.title,
...(rfc.authors.length > 0 ?
{
author: {
name: rfcMetadata.authors
name: rfc.authors
}
}
: {}),
date: { month, year },
format: {
'file-format': formats
},
'page-count': rfcMetadata.pages,
'page-count': rfc.pages,
keywords: {
// @ts-expect-error update when client provides that
kw: rfcMetadata.keywords
kw: rfc.keywords
},
...(abstractParagraphs.length > 0 ?
{
Expand All @@ -117,19 +120,18 @@ const renderRFCs = async ({
}
: {}),
draft: 'draft-ietf-lamps-cms-cek-hkdf-sha256-05',
'current-status': rfcMetadata.status.slug,
'publication-status': rfcMetadata.status.slug,
stream: rfcMetadata.stream.name,
area: rfcMetadata.area,
wg_acronym: rfcMetadata.group.acronym,
'current-status': rfc.status.slug,
'publication-status': rfc.status.slug,
stream: rfc.stream.name,
area: rfc.area,
wg_acronym: rfc.group.acronym,
doi:
rfcMetadata.identifiers?.find(
(identifier) => identifier.type === 'doi'
)?.value ?? undefined
rfc.identifiers?.find((identifier) => identifier.type === 'doi')
?.value ?? undefined
}
}

const xml = builder.build(rfc)
const xml = builder.build(rfcForXml)

push(xml)
})
Expand Down
96 changes: 62 additions & 34 deletions client/utilities/rfc-index.xml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import fs from 'node:fs'
import path from 'node:path'
import { z } from 'zod'
import { zip } from 'lodash'
import { vi, describe, beforeEach, afterEach, test, expect } from 'vitest'
import { XMLParser } from 'fast-xml-parser'

Expand All @@ -11,6 +12,7 @@ import {
ApiClient,
type PaginatedRfcMetadataList
} from '~/generated/red-client'
import { parseRFCId } from './rfc'

const originalXMLString = fs
.readFileSync(path.join(import.meta.dirname, 'rfc-index.xml'), 'utf-8')
Expand All @@ -28,11 +30,14 @@ type DocListResponse = Awaited<ReturnType<ApiClient['red']['docList']>>
* The following Zod schema is the minimal amount of structure needed to parse BCP/FYI/RFC/STD
* entries with typing.
*
* Why not refine the z.any() type further? That's unnecessary because we have reference XML (originalXML)
* We don't need to refine the EntrySchema value further because we have reference XML (originalXML)
* to parse too which means the XMLParser should produce the same data from the same XML so we can use
* vitest to diff them. This also avoids any maintenance burden of keeping a Zod schema in sync with the
* output of XMLBuilder.
* vitest to diff them rather than validating their exact types. This also avoids any maintenance burden
* of keeping a Zod schema in sync with the output of XMLBuilder.
*/
const EntryValueSchema = z
.record(z.string(), z.record(z.string(), z.any()).array())
.array()
const EntrySchema = z.record(
z.enum([
'bcp-entry',
Expand All @@ -41,9 +46,11 @@ const EntrySchema = z.record(
'rfc-not-issued-entry',
'std-entry'
]),
z.any()
EntryValueSchema
)
const EntriesSchema = EntrySchema.array()
type Entry = z.infer<typeof EntrySchema>
type RFCEntry = Required<Pick<Entry, 'rfc-entry'>>

type TestHelperResponses = {
oldestRfcResponse: DocListResponse
Expand Down Expand Up @@ -93,29 +100,38 @@ const testHelper = (responses: TestHelperResponses) =>
})()
})

const filterByEntryTypeFactory =
(key: keyof z.infer<typeof EntrySchema>) =>
(entry: z.infer<typeof EntrySchema>) => {
return key in entry
}
const filterByRFCEntry = (entry: Entry): entry is RFCEntry => {
return 'rfc-entry' in entry
}

const filterByRFCEntry = filterByEntryTypeFactory('rfc-entry')
// const summariseEntries = (entries: any[]) =>
// entries.reduce(
// (acc, entry) =>
// Object.keys(entry).reduce((acc, key, _index, arr) => {
// if (arr.length === 0) {
// throw Error(`Entry has no keys?`)
// }
// if (!acc[key]) {
// acc[key] = 0
// }
// acc[key]++
// return acc
// }, acc),
// {}
// )

const summariseEntries = (entries: any[]) =>
entries.reduce(
(acc, entry) =>
Object.keys(entry).reduce((acc, key, _index, arr) => {
if (arr.length === 0) {
throw Error(`Entry has no keys?`)
}
if (!acc[key]) {
acc[key] = 0
}
acc[key]++
return acc
}, acc),
{}
)
const getRFCNumber = (rfcEntry: RFCEntry): string => {
const docIdItem = rfcEntry['rfc-entry'].find((item) => {
return !!item['doc-id']
})
if (!docIdItem) throw Error(`Couldn't find doc-id in ${rfcEntry}`)
const rfcNumber = docIdItem['doc-id'][0]['#text']
const typeofRFCNumber = typeof rfcNumber !== 'string'
if (typeofRFCNumber) {
throw Error(`Expected RFCNumber to be a string but was ${typeofRFCNumber}`)
}
return parseRFCId(rfcNumber).number
}

describe('renderRfcIndexDotXml', () => {
beforeEach(() => {
Expand All @@ -136,14 +152,15 @@ describe('renderRfcIndexDotXml', () => {
expect(originalXML[1]).toHaveProperty('rfc-index')
const originalXMLEntries = originalXML[1]['rfc-index']
expect(originalXMLEntries.length).toBeGreaterThan(10)
const originalParseResult = EntriesSchema.safeParse(originalXMLEntries)
const originalParsedEntries = originalParseResult.data
const originalParsedXML = EntriesSchema.safeParse(originalXMLEntries)
const originalParsedEntries = originalParsedXML.data
if (!originalParsedEntries) {
console.log(originalParseResult.error)
console.log(originalParsedXML.error)
throw Error('Expected some results')
}
expect(originalParseResult.success).toBeTruthy()
const originalRFCs = originalParsedEntries.filter(filterByRFCEntry)
expect(originalParsedXML.success).toBeTruthy()
const originalRFCs: RFCEntry[] =
originalParsedEntries.filter(filterByRFCEntry)
expect(originalRFCs.length).toBeGreaterThan(10)

const result = await testHelper({
Expand All @@ -159,21 +176,32 @@ describe('renderRfcIndexDotXml', () => {
expect(resultXML[1]).toHaveProperty('rfc-index')
const resultXMLEntries = resultXML[1]['rfc-index']
expect(resultXMLEntries.length).toBeGreaterThan(10)
const resultEntries = EntriesSchema.parse(resultXMLEntries)
const resultRFCs = resultEntries.filter(filterByRFCEntry)
const resultParsedXML = EntriesSchema.safeParse(resultXMLEntries)
const resultParsedEntries = resultParsedXML.data
if (!resultParsedEntries) {
console.log(resultParsedXML.error)
throw Error('Expected some results')
}
expect(resultParsedXML.success).toBeTruthy()

const resultRFCs: RFCEntry[] = resultParsedEntries.filter(filterByRFCEntry)
expect(resultRFCs.length).toBeGreaterThan(10)

// Intentionally not using test.each() because that would continue running tests after one fails
// whereas this will fail early and compete the tests much quicker
resultRFCs.forEach((resultRFC, i) => {
const originalRFC = originalRFCs[i]
const originalRFCNumber = getRFCNumber(originalRFC)
const resultRFCNumber = getRFCNumber(resultRFC)

expect(resultRFCNumber).toEqual(originalRFCNumber)

if (
// FIXME: enable checking more RFCs
i < 14
) {
console.log(`Checking RFC${i + 1}`)
expect(resultRFC).toBe(originalRFC)
console.log(`Checking ${originalRFCNumber}`, resultRFC, originalRFC)
expect(resultRFC).toEqual(originalRFC)

Check failure on line 204 in client/utilities/rfc-index.xml.test.ts

View workflow job for this annotation

GitHub Actions / build

utilities/rfc-index.xml.test.ts > renderRfcIndexDotXml > compare against original rendering

AssertionError: expected { 'rfc-entry': [ { …(1) }, …(13) ] } to deeply equal { 'rfc-entry': [ { …(1) }, …(9) ] } - Expected + Received Object { "rfc-entry": Array [ Object { "doc-id": Array [ Object { - "#text": "RFC1", + "#text": "rfc1", }, ], }, Object { "title": Array [ Object { "#text": "Host Software", }, ], }, Object { "author": Array [ Object { "name": Array [ Object { - "#text": "S. Crocker", + "name": Array [ + Object { + "#text": "Steve Crocker", + }, + ], + }, + Object { + "person": Array [ + Object { + "#text": 0, + }, + ], }, ], }, ], }, Object { "date": Array [ Object { "month": Array [ Object { "#text": "April", }, ], }, Object { "year": Array [ Object { "#text": 1969, }, ], }, ], }, Object { "format": Array [ + Object { + "file-format": Array [ Object { + "#text": "HTML", + }, + ], + }, + Object { "file-format": Array [ Object { - "#text": "ASCII", + "#text": "TEXT", }, ], }, Object { "file-format": Array [ Object { - "#text": "HTML", + "#text": "PDF", + }, + ], + }, + Object { + "file-format": Array [ + Object { + "#text": "XML", }, ], }, ], }, Object { "page-count": Array [ Object { "#text": 11, + }, + ], + }, + Object { + "keywords": Array [], + }, + Object { + "abstract": Array [ + Object { + "p": Array [], + }, + ], + }, + Object { + "draft": Array [ + Object { + "#text": "draft-ietf-lamps-cms-cek-hkdf-sha256-05", }, ], }, Object { "current-status": Array [ Object { - "#text": "UNKNOWN", + "#text": "unknown", }, ], }, Object { "publication-status": Array [ Object { - "#text": "UNKNOWN", + "#text": "unknown", }, ], }, Object { "stream": Array [ Object { "#text": "Legacy", + }, + ], + }, + Object { + "wg_acronym": Array [ + Object { + "#text": "none", }, ], }, Object { "doi": Array [ Object { "#text": "10.17487/RFC0001", }, ], }, ], } ❯ utilities/rfc-index.xml.test.ts:204:27 ❯ utilities/rfc-index.xml.test.ts:192:16
}
})
})
Expand Down
126 changes: 125 additions & 1 deletion client/utilities/rfc.mocks.ts

Large diffs are not rendered by default.

0 comments on commit 84c2845

Please sign in to comment.