verify-smoke-matrix.mjs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { existsSync } from 'node:fs'
  2. import { readFile } from 'node:fs/promises'
  3. import { extname, join } from 'node:path'
  4. import { readCoreRendererDefinitions } from './lib/format-support.mjs'
  5. const sourceRoot = process.cwd()
  6. const matrixPath = join(sourceRoot, 'ecosystem', 'smoke-matrix.json')
  7. const wrapperManifestPath = join(sourceRoot, 'ecosystem', 'wrappers.json')
  8. const examplesRoot = join(sourceRoot, 'apps', 'viewer-demo', 'public', 'example')
  9. const vitePluginSourcePath = join(sourceRoot, 'packages', 'presets', 'vite-plugin', 'src', 'index.ts')
  10. const pluginEnginePackages = new Set([
  11. '@file-viewer/pptx',
  12. '@file-viewer/eda-layout',
  13. '@file-viewer/eda-orcad',
  14. '@file-viewer/geometry-engine'
  15. ])
  16. function fail(message) {
  17. throw new Error(`[smoke-matrix] ${message}`)
  18. }
  19. async function readJson(path) {
  20. return JSON.parse(await readFile(path, 'utf8'))
  21. }
  22. function assertUnique(items, label, selectId = item => item.id) {
  23. const seen = new Set()
  24. for (const item of items) {
  25. const id = selectId(item)
  26. if (!id) {
  27. fail(`${label} contains an item without id`)
  28. }
  29. if (seen.has(id)) {
  30. fail(`${label} contains duplicate id ${id}`)
  31. }
  32. seen.add(id)
  33. }
  34. }
  35. function assertSampleExists(sample, label) {
  36. const samplePath = join(examplesRoot, sample)
  37. if (!existsSync(samplePath)) {
  38. fail(`${label} sample does not exist: apps/viewer-demo/public/example/${sample}`)
  39. }
  40. }
  41. function sampleExtension(sample, explicitExtension) {
  42. if (explicitExtension) {
  43. return explicitExtension.toLowerCase()
  44. }
  45. return extname(sample).replace(/^\./, '').toLowerCase()
  46. }
  47. function quotedValues(text) {
  48. return [...String(text).matchAll(/'([^']+)'/g)].map(match => match[1])
  49. }
  50. async function readViteRendererPackageDescriptors() {
  51. const source = await readFile(vitePluginSourcePath, 'utf8')
  52. const rendererModuleMatch = source.match(
  53. /const rendererModules:[\s\S]*?=\s*\[([\s\S]*?)\]\s*\n\nconst descriptorsById/
  54. )
  55. if (!rendererModuleMatch) {
  56. fail('Could not find rendererModules in @file-viewer/vite-plugin source')
  57. }
  58. const descriptors = new Map()
  59. const rendererObjectPattern = /\{\s*id:\s*'([^']+)'([\s\S]*?)chunkName:\s*'[^']+'\s*\}/g
  60. for (const match of rendererModuleMatch[1].matchAll(rendererObjectPattern)) {
  61. const id = match[1]
  62. const body = match[2]
  63. const packageName = body.match(/packageName:\s*'([^']+)'/)?.[1]
  64. const rendererIdsSource = body.match(/rendererIds:\s*\[([\s\S]*?)\]/)?.[1]
  65. if (!packageName || !rendererIdsSource) {
  66. fail(`Could not parse renderer package descriptor for ${id}`)
  67. }
  68. if (descriptors.has(packageName)) {
  69. fail(`Vite renderer package descriptor contains duplicate package ${packageName}`)
  70. }
  71. const rendererIds = quotedValues(rendererIdsSource)
  72. if (!rendererIds.length) {
  73. fail(`Vite renderer package descriptor ${packageName} does not expose renderer ids`)
  74. }
  75. descriptors.set(packageName, {
  76. id,
  77. packageName,
  78. rendererIds
  79. })
  80. }
  81. if (!descriptors.size) {
  82. fail('No Vite renderer package descriptors were detected')
  83. }
  84. return descriptors
  85. }
  86. function assertRendererSample(entry, label) {
  87. const renderer = renderers.get(entry.rendererId)
  88. if (!renderer) {
  89. fail(`${label} references unknown renderer ${entry.rendererId}`)
  90. }
  91. const extension = sampleExtension(entry.sample, entry.extension)
  92. if (!renderer.extensions.has(extension)) {
  93. fail(`${label} extension ${extension} is not supported by renderer ${entry.rendererId}`)
  94. }
  95. assertSampleExists(entry.sample, label)
  96. }
  97. const [matrix, wrapperManifest, renderers, viteRendererPackages] = await Promise.all([
  98. readJson(matrixPath),
  99. readJson(wrapperManifestPath),
  100. readCoreRendererDefinitions(sourceRoot),
  101. readViteRendererPackageDescriptors()
  102. ])
  103. if (matrix.schemaVersion !== 1) {
  104. fail(`Unsupported schemaVersion ${matrix.schemaVersion}`)
  105. }
  106. const surfaces = new Set(matrix.surfaces || [])
  107. for (const surface of ['main-demo', 'compare-page', 'script-tag']) {
  108. if (!surfaces.has(surface)) {
  109. fail(`Missing required smoke surface ${surface}`)
  110. }
  111. }
  112. assertUnique(matrix.wrappers || [], 'wrappers')
  113. const matrixWrappers = new Map((matrix.wrappers || []).map(wrapper => [wrapper.id, wrapper]))
  114. for (const wrapper of wrapperManifest.wrappers) {
  115. const matrixWrapper = matrixWrappers.get(wrapper.id)
  116. if (!matrixWrapper) {
  117. fail(`Smoke matrix is missing wrapper ${wrapper.id}`)
  118. }
  119. if (matrixWrapper.packageName !== wrapper.packageName) {
  120. fail(`Wrapper ${wrapper.id} package mismatch: ${matrixWrapper.packageName} !== ${wrapper.packageName}`)
  121. }
  122. if (!surfaces.has(matrixWrapper.surface)) {
  123. fail(`Wrapper ${wrapper.id} references unknown surface ${matrixWrapper.surface}`)
  124. }
  125. }
  126. assertUnique(matrix.cases || [], 'cases')
  127. const coveredRendererIds = new Set()
  128. const coveredFamilies = new Set()
  129. for (const smokeCase of matrix.cases || []) {
  130. assertRendererSample(smokeCase, `Case ${smokeCase.id}`)
  131. if (!Array.isArray(smokeCase.surfaces) || !smokeCase.surfaces.length) {
  132. fail(`Case ${smokeCase.id} must declare at least one surface`)
  133. }
  134. for (const surface of smokeCase.surfaces) {
  135. if (!surfaces.has(surface)) {
  136. fail(`Case ${smokeCase.id} references unknown surface ${surface}`)
  137. }
  138. }
  139. if (!Array.isArray(smokeCase.assertions) || !smokeCase.assertions.length) {
  140. fail(`Case ${smokeCase.id} must declare at least one assertion`)
  141. }
  142. coveredRendererIds.add(smokeCase.rendererId)
  143. coveredFamilies.add(smokeCase.family)
  144. }
  145. if (matrix.coverage?.requireAllRendererIds) {
  146. for (const rendererId of renderers.keys()) {
  147. if (!coveredRendererIds.has(rendererId)) {
  148. fail(`Smoke matrix does not cover renderer ${rendererId}`)
  149. }
  150. }
  151. }
  152. let coveredRendererPackageCount = 0
  153. if (matrix.coverage?.requireAllRendererPackages) {
  154. for (const rendererPackage of wrapperManifest.renderers || []) {
  155. if (pluginEnginePackages.has(rendererPackage.packageName)) {
  156. continue
  157. }
  158. const descriptor = viteRendererPackages.get(rendererPackage.packageName)
  159. if (!descriptor) {
  160. fail(`Smoke matrix cannot resolve renderer package ${rendererPackage.packageName} through @file-viewer/vite-plugin`)
  161. }
  162. const missingRendererIds = descriptor.rendererIds.filter(rendererId => !coveredRendererIds.has(rendererId))
  163. if (missingRendererIds.length) {
  164. fail(
  165. `Smoke matrix does not cover renderer package ${rendererPackage.packageName}; missing renderer ids: ${missingRendererIds.join(', ')}`
  166. )
  167. }
  168. coveredRendererPackageCount += 1
  169. }
  170. }
  171. for (const family of matrix.coverage?.requiredFormatFamilies || []) {
  172. if (!coveredFamilies.has(family)) {
  173. fail(`Smoke matrix does not cover required family ${family}`)
  174. }
  175. }
  176. assertUnique(matrix.wrapperCases || [], 'wrapperCases')
  177. const wrapperCaseIds = new Set()
  178. for (const wrapperCase of matrix.wrapperCases || []) {
  179. if (!matrixWrappers.has(wrapperCase.wrapperId)) {
  180. fail(`Wrapper case ${wrapperCase.id} references unknown wrapper ${wrapperCase.wrapperId}`)
  181. }
  182. if (!surfaces.has(wrapperCase.surface)) {
  183. fail(`Wrapper case ${wrapperCase.id} references unknown surface ${wrapperCase.surface}`)
  184. }
  185. assertSampleExists(wrapperCase.sample, `Wrapper case ${wrapperCase.id}`)
  186. wrapperCaseIds.add(wrapperCase.wrapperId)
  187. }
  188. for (const wrapper of wrapperManifest.wrappers) {
  189. if (!wrapperCaseIds.has(wrapper.id)) {
  190. fail(`Smoke matrix does not include a wrapper case for ${wrapper.id}`)
  191. }
  192. }
  193. const requiredWrapperFamilies = matrix.wrapperCoverage?.requiredFamilies || []
  194. assertUnique(requiredWrapperFamilies, 'wrapperCoverage.requiredFamilies', item => item.family)
  195. if (matrix.wrapperCoverage?.requireEveryWrapper && !requiredWrapperFamilies.length) {
  196. fail('wrapperCoverage.requireEveryWrapper is enabled but no requiredFamilies were declared')
  197. }
  198. for (const family of requiredWrapperFamilies) {
  199. assertRendererSample(family, `Wrapper required family ${family.family}`)
  200. if (!Array.isArray(family.assertions) || !family.assertions.length) {
  201. fail(`Wrapper required family ${family.family} must declare at least one assertion`)
  202. }
  203. }
  204. let wrapperTargetCount = 0
  205. if (matrix.wrapperCoverage?.requireEveryWrapper) {
  206. for (const wrapper of wrapperManifest.wrappers) {
  207. const matrixWrapper = matrixWrappers.get(wrapper.id)
  208. if (!matrixWrapper) {
  209. fail(`Wrapper coverage cannot find wrapper ${wrapper.id}`)
  210. }
  211. for (const family of requiredWrapperFamilies) {
  212. wrapperTargetCount += 1
  213. }
  214. }
  215. }
  216. console.log(
  217. `[smoke-matrix] Verified ${matrix.cases.length} format cases, ${matrix.wrapperCases.length} wrapper cases, ${wrapperTargetCount} wrapper family targets, ${renderers.size} renderer pipelines, and ${coveredRendererPackageCount} renderer packages.`
  218. )