runtime-payload-smoke.mjs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /** Exercise filtered Desktop native and HTML dependencies under its bundled Node. */
  2. import assert from 'node:assert/strict'
  3. import { closeSync, mkdtempSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs'
  4. import { rm } from 'node:fs/promises'
  5. import { createRequire } from 'node:module'
  6. import { tmpdir } from 'node:os'
  7. import { join, resolve } from 'node:path'
  8. const runtime = process.argv[2]
  9. assert.ok(runtime, 'Pass the filtered resources/dsh directory')
  10. const root = resolve(runtime)
  11. const descriptor = JSON.parse(readFileSync(join(root, 'desktop-runtime.json'), 'utf8'))
  12. assert.equal(process.versions.node, descriptor.release.nodeVersion, 'Run with the bundled Node version')
  13. assert.equal(process.platform, descriptor.platform)
  14. assert.equal(process.arch, descriptor.arch)
  15. const requireRuntime = createRequire(join(root, 'package.json'))
  16. const scratch = mkdtempSync(join(tmpdir(), 'dsh-runtime-payload-'))
  17. /** Spawn only a fixed Node program and await the terminal's drained exit event. */
  18. async function checkPty() {
  19. const pty = requireRuntime('node-pty')
  20. const script = join(scratch, 'pty.cjs')
  21. writeFileSync(script, "process.stdout.write('runtime-payload-pty-ok\\n')\n", { flag: 'wx', mode: 0o600 })
  22. const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => (
  23. /^(?:path|systemroot|windir|comspec)$/iu.test(name)
  24. )))
  25. Object.assign(env, { HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch })
  26. const terminal = pty.spawn(process.execPath, [script], { cwd: scratch, env, cols: 80, rows: 24 })
  27. let output = ''
  28. let exited = false
  29. let timedOut = false
  30. let exitSubscription
  31. const exit = new Promise(resolveExit => {
  32. exitSubscription = terminal.onExit(event => {
  33. exited = true
  34. resolveExit(event)
  35. })
  36. })
  37. const dataSubscription = terminal.onData(data => { output += data })
  38. let timer
  39. try {
  40. const deadline = new Promise((_, reject) => {
  41. timer = setTimeout(() => {
  42. timedOut = true
  43. reject(new Error('Packaged PTY did not exit within 45 seconds'))
  44. }, 45_000)
  45. })
  46. const result = await Promise.race([exit, deadline])
  47. assert.equal(timedOut, false)
  48. assert.ok(result.signal === undefined || result.signal === 0, 'PTY exited without a signal')
  49. assert.equal(result.exitCode, 0)
  50. assert.match(output, /runtime-payload-pty-ok/u)
  51. } finally {
  52. clearTimeout(timer)
  53. dataSubscription.dispose()
  54. try {
  55. // node-pty's Windows natural-exit event closes output but leaves its ConPTY worker owned by kill().
  56. if (!exited || process.platform === 'win32') terminal.kill()
  57. await exit
  58. } finally {
  59. exitSubscription.dispose()
  60. }
  61. }
  62. }
  63. /** fs-ext implements seek on Windows through SetFilePointerEx and on POSIX through lseek. */
  64. function checkFsExt() {
  65. const fsExt = requireRuntime('fs-ext')
  66. const file = join(scratch, 'seek.txt')
  67. writeFileSync(file, 'abcdef', { flag: 'wx', mode: 0o600 })
  68. const fd = openSync(file, 'r')
  69. try {
  70. assert.equal(fsExt.seekSync(fd, 2, fsExt.constants.SEEK_SET), 2)
  71. const bytes = Buffer.alloc(4)
  72. assert.equal(readSync(fd, bytes, 0, bytes.length, null), 4)
  73. assert.equal(bytes.toString(), 'cdef')
  74. } finally {
  75. closeSync(fd)
  76. }
  77. }
  78. /** Resolve one system function through Koffi's packaged native module. */
  79. function checkKoffi() {
  80. const koffi = requireRuntime('koffi')
  81. const library = koffi.load(process.platform === 'win32' ? 'kernel32.dll' : null)
  82. try {
  83. const getPid = process.platform === 'win32'
  84. ? library.func('uint32_t __stdcall GetCurrentProcessId(void)')
  85. : library.func('int getpid(void)')
  86. assert.equal(getPid(), process.pid)
  87. } finally {
  88. library.unload()
  89. }
  90. }
  91. /** Encode and decode a pixel through the packaged libvips binary. */
  92. async function checkSharp() {
  93. const sharp = requireRuntime('sharp')
  94. const pixel = Buffer.from([17, 103, 231])
  95. const png = await sharp(pixel, { raw: { width: 1, height: 1, channels: 3 } }).png().toBuffer()
  96. const decoded = await sharp(png).raw().toBuffer({ resolveWithObject: true })
  97. assert.equal(decoded.info.width, 1)
  98. assert.equal(decoded.info.height, 1)
  99. assert.equal(decoded.info.channels, 3)
  100. assert.deepEqual(decoded.data, pixel)
  101. }
  102. /** Exercise Domino parsing through the HTML converter and GFM plugin used by web_fetch. */
  103. function checkHtml() {
  104. const Turndown = requireRuntime('turndown')
  105. const { gfm } = requireRuntime('@joplin/turndown-plugin-gfm')
  106. const converter = new Turndown({ bulletListMarker: '-' })
  107. converter.use(gfm)
  108. const markdown = converter.turndown('<p>A &amp; B &copy;</p><ul><li>first</li><li>second</li></ul>'
  109. + '<table><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td>x</td><td>7</td></tr></tbody></table>')
  110. assert.match(markdown, /A & B ©/u)
  111. assert.match(markdown, /-\s+first\n-\s+second/u)
  112. assert.match(markdown, /\| Name \| Value \|/u)
  113. assert.match(markdown, /\| x\s+\| 7\s+\|/u)
  114. }
  115. try {
  116. checkFsExt()
  117. checkKoffi()
  118. await checkSharp()
  119. checkHtml()
  120. await checkPty()
  121. } finally {
  122. // This private tree contains only fixture files; Windows may release handles after terminal exit.
  123. await rm(scratch, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
  124. }
  125. // Natural event-loop drain includes node-pty's worker and console-list helper teardown.
  126. process.once('beforeExit', () => {
  127. console.log(JSON.stringify({ node: process.versions.node, platform: process.platform, arch: process.arch,
  128. fsExt: true, koffi: true, sharp: true, html: true, pty: true }))
  129. })