goscript 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/compiler/lowered-program.go +1 -0
  2. package/compiler/lowering.go +715 -44
  3. package/compiler/override-registry_test.go +43 -0
  4. package/compiler/skeleton_test.go +464 -12
  5. package/compiler/typescript-emitter.go +28 -2
  6. package/dist/gs/builtin/channel.js +36 -9
  7. package/dist/gs/builtin/channel.js.map +1 -1
  8. package/dist/gs/builtin/type.js +8 -3
  9. package/dist/gs/builtin/type.js.map +1 -1
  10. package/dist/gs/bytes/bytes.gs.d.ts +7 -5
  11. package/dist/gs/bytes/bytes.gs.js +10 -4
  12. package/dist/gs/bytes/bytes.gs.js.map +1 -1
  13. package/dist/gs/crypto/sha1/index.d.ts +5 -0
  14. package/dist/gs/crypto/sha1/index.js +106 -0
  15. package/dist/gs/crypto/sha1/index.js.map +1 -0
  16. package/dist/gs/fmt/fmt.d.ts +1 -1
  17. package/dist/gs/fmt/fmt.js +64 -3
  18. package/dist/gs/fmt/fmt.js.map +1 -1
  19. package/dist/gs/io/io.d.ts +8 -5
  20. package/dist/gs/io/io.js +20 -2
  21. package/dist/gs/io/io.js.map +1 -1
  22. package/dist/gs/net/http/httptest/index.js +7 -5
  23. package/dist/gs/net/http/httptest/index.js.map +1 -1
  24. package/dist/gs/net/http/index.d.ts +8 -0
  25. package/dist/gs/net/http/index.js +139 -10
  26. package/dist/gs/net/http/index.js.map +1 -1
  27. package/dist/gs/os/zero_copy_posix.gs.js +1 -1
  28. package/dist/gs/os/zero_copy_posix.gs.js.map +1 -1
  29. package/gs/builtin/channel.ts +47 -9
  30. package/gs/builtin/runtime-contract.test.ts +33 -0
  31. package/gs/builtin/type.ts +12 -3
  32. package/gs/bytes/bytes.gs.ts +19 -10
  33. package/gs/bytes/bytes.test.ts +17 -0
  34. package/gs/context/context.test.ts +5 -1
  35. package/gs/crypto/sha1/index.test.ts +28 -0
  36. package/gs/crypto/sha1/index.ts +130 -0
  37. package/gs/crypto/sha1/meta.json +8 -0
  38. package/gs/fmt/fmt.test.ts +20 -0
  39. package/gs/fmt/fmt.ts +75 -5
  40. package/gs/github.com/aperturerobotics/util/conc/index.test.ts +1 -1
  41. package/gs/io/io.test.ts +64 -0
  42. package/gs/io/io.ts +30 -12
  43. package/gs/net/http/httptest/index.test.ts +34 -2
  44. package/gs/net/http/httptest/index.ts +23 -8
  45. package/gs/net/http/index.test.ts +30 -0
  46. package/gs/net/http/index.ts +159 -10
  47. package/gs/os/zero_copy_posix.gs.ts +1 -2
  48. package/gs/sync/meta.json +1 -0
  49. package/package.json +1 -1
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { New, Sum } from './index.js'
5
+
6
+ describe('crypto/sha1 override', () => {
7
+ it('matches Node digest', async () => {
8
+ const data = new TextEncoder().encode('goscript sha1')
9
+
10
+ expect(toHex(await Sum(data))).toBe(nodeHash(data))
11
+ })
12
+
13
+ it('supports incremental hash.Hash use', async () => {
14
+ const h = New()
15
+ h.Write(new TextEncoder().encode('go'))
16
+ h.Write(new TextEncoder().encode('script'))
17
+
18
+ expect(toHex(await h.Sum(null))).toBe(nodeHash(new TextEncoder().encode('goscript')))
19
+ })
20
+ })
21
+
22
+ function nodeHash(data: Uint8Array): string {
23
+ return createHash('sha1').update(data).digest('hex')
24
+ }
25
+
26
+ function toHex(value: Uint8Array): string {
27
+ return Buffer.from(value).toString('hex')
28
+ }
@@ -0,0 +1,130 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+ import {
3
+ getHostRuntime,
4
+ type NodeCryptoHash,
5
+ } from '@goscript/builtin/hostio.js'
6
+
7
+ export const Size = 20
8
+ export const BlockSize = 64
9
+
10
+ class Sha1Error {
11
+ constructor(private readonly message: string) {}
12
+
13
+ Error(): string {
14
+ return this.message
15
+ }
16
+ }
17
+
18
+ class Digest {
19
+ private chunks: Uint8Array[] = []
20
+ private dataLength = 0
21
+ private hash: NodeCryptoHash | null
22
+ private canCopyHash: boolean
23
+
24
+ constructor() {
25
+ this.hash = createNodeHash()
26
+ this.canCopyHash = typeof this.hash?.copy === 'function'
27
+ }
28
+
29
+ Write(p: $.Bytes): [number, $.GoError] {
30
+ const bytes = $.bytesToUint8Array(p)
31
+ this.hash?.update(bytes)
32
+ if (!this.canCopyHash) {
33
+ this.chunks.push(bytes.slice())
34
+ this.dataLength += bytes.length
35
+ }
36
+ return [bytes.length, null]
37
+ }
38
+
39
+ async Sum(b: $.Bytes): Promise<$.Bytes> {
40
+ const digest =
41
+ this.canCopyHash ?
42
+ new Uint8Array(this.hash!.copy!().digest())
43
+ : await sum(this.snapshotBytes())
44
+ return appendDigest($.bytesToUint8Array(b), digest)
45
+ }
46
+
47
+ Reset(): void {
48
+ this.chunks = []
49
+ this.dataLength = 0
50
+ this.hash = createNodeHash()
51
+ this.canCopyHash = typeof this.hash?.copy === 'function'
52
+ }
53
+
54
+ Size(): number {
55
+ return Size
56
+ }
57
+
58
+ BlockSize(): number {
59
+ return BlockSize
60
+ }
61
+
62
+ private snapshotBytes(): Uint8Array {
63
+ return concatChunks(this.chunks, this.dataLength)
64
+ }
65
+ }
66
+
67
+ export function New(): any {
68
+ return new Digest()
69
+ }
70
+
71
+ export async function Sum(data: $.Bytes): Promise<Uint8Array> {
72
+ return sum(data)
73
+ }
74
+
75
+ async function sum(data: $.Bytes): Promise<Uint8Array> {
76
+ const hash = createNodeHash()
77
+ if (hash != null) {
78
+ return new Uint8Array(hash.update($.bytesToUint8Array(data)).digest())
79
+ }
80
+
81
+ const subtle = subtleCrypto()
82
+ if (subtle == null) {
83
+ throw new Error(
84
+ new Sha1Error('crypto/sha1: WebCrypto digest is unavailable').Error(),
85
+ )
86
+ }
87
+
88
+ const digest = await subtle.digest(
89
+ 'SHA-1',
90
+ $.bytesToUint8Array(data) as unknown as BufferSource,
91
+ )
92
+ return new Uint8Array(digest)
93
+ }
94
+
95
+ function appendDigest(prefix: Uint8Array, digest: Uint8Array): Uint8Array {
96
+ const out = new Uint8Array(prefix.length + digest.length)
97
+ out.set(prefix)
98
+ out.set(digest, prefix.length)
99
+ return out
100
+ }
101
+
102
+ function createNodeHash(): NodeCryptoHash | null {
103
+ const nodeCrypto = getHostRuntime().nodeCrypto
104
+ if (!nodeCrypto?.createHash) {
105
+ return null
106
+ }
107
+ try {
108
+ return nodeCrypto.createHash('sha1')
109
+ } catch {
110
+ return null
111
+ }
112
+ }
113
+
114
+ function concatChunks(chunks: Uint8Array[], length: number): Uint8Array {
115
+ const out = new Uint8Array(length)
116
+ let offset = 0
117
+ for (const chunk of chunks) {
118
+ out.set(chunk, offset)
119
+ offset += chunk.length
120
+ }
121
+ return out
122
+ }
123
+
124
+ function subtleCrypto(): SubtleCrypto | null {
125
+ const crypto = globalThis.crypto
126
+ if (crypto?.subtle && typeof crypto.subtle.digest === 'function') {
127
+ return crypto.subtle
128
+ }
129
+ return null
130
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "asyncFunctions": {
3
+ "Sum": true
4
+ },
5
+ "asyncMethods": {
6
+ "Digest.Sum": true
7
+ }
8
+ }
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest'
2
2
  import { resetHostRuntimeForTests } from '@goscript/builtin/hostio.js'
3
+ import * as $ from '@goscript/builtin/index.js'
3
4
  import * as fmt from './fmt.js'
4
5
 
5
6
  const originalDeno = (globalThis as any).Deno
@@ -187,3 +188,22 @@ describe('fmt parseFormat basic cases', () => {
187
188
  expect(fmt.Sprintf('%c', 65)).toBe('A')
188
189
  })
189
190
  })
191
+
192
+ describe('fmt scanning', () => {
193
+ it('scans decimal fields separated by literals', () => {
194
+ const start = $.varRef(0)
195
+ const end = $.varRef(0)
196
+
197
+ const [n, err] = fmt.Sscanf(
198
+ 'bytes=12-34',
199
+ 'bytes=%d-%d',
200
+ $.interfaceValue(start, '*int64'),
201
+ $.interfaceValue(end, '*int64'),
202
+ )
203
+
204
+ expect(err).toBeNull()
205
+ expect(n).toBe(2)
206
+ expect(start.value).toBe(12)
207
+ expect(end.value).toBe(34)
208
+ })
209
+ })
package/gs/fmt/fmt.ts CHANGED
@@ -488,12 +488,82 @@ export function Sscan(_str: string, ..._a: any[]): [number, $.GoError | null] {
488
488
  }
489
489
 
490
490
  export function Sscanf(
491
- _str: string,
492
- _format: string,
493
- ..._a: any[]
491
+ str: string,
492
+ format: string,
493
+ ...a: any[]
494
494
  ): [number, $.GoError | null] {
495
- // TODO: Implement formatted scanning from string
496
- return [0, $.newError('Sscanf not implemented')]
495
+ const parts = buildScanPattern(format)
496
+ if (parts == null) {
497
+ return [0, $.newError(`unsupported Sscanf format: ${format}`)]
498
+ }
499
+ const match = parts.pattern.exec(str)
500
+ if (match == null) {
501
+ return [0, $.newError('input does not match format')]
502
+ }
503
+
504
+ let assigned = 0
505
+ for (let i = 0; i < parts.verbs.length && i < a.length; i++) {
506
+ const raw = match[i + 1]
507
+ const value = parts.verbs[i] === 'd' ? Number.parseInt(raw, 10) : raw
508
+ if (!assignScanValue(a[i], value)) {
509
+ return [assigned, $.newError('scan destination is not assignable')]
510
+ }
511
+ assigned++
512
+ }
513
+ return [assigned, null]
514
+ }
515
+
516
+ function buildScanPattern(
517
+ format: string,
518
+ ): { pattern: RegExp; verbs: string[] } | null {
519
+ let source = '^'
520
+ const verbs: string[] = []
521
+ for (let i = 0; i < format.length; i++) {
522
+ const ch = format[i]
523
+ if (ch !== '%') {
524
+ source += /\s/.test(ch) ? '\\s+' : escapeRegExp(ch)
525
+ continue
526
+ }
527
+ const verb = format[++i]
528
+ if (verb === '%') {
529
+ source += '%'
530
+ continue
531
+ }
532
+ if (verb === 'd') {
533
+ source += '([+-]?\\d+)'
534
+ verbs.push(verb)
535
+ continue
536
+ }
537
+ if (verb === 's') {
538
+ source += '(\\S+)'
539
+ verbs.push(verb)
540
+ continue
541
+ }
542
+ return null
543
+ }
544
+ source += '$'
545
+ return { pattern: new RegExp(source), verbs }
546
+ }
547
+
548
+ function assignScanValue(target: any, value: string | number): boolean {
549
+ const ref =
550
+ $.isVarRef(target) ? target
551
+ : (
552
+ target != null &&
553
+ typeof target === 'object' &&
554
+ $.isVarRef(target.__goValue)
555
+ ) ?
556
+ target.__goValue
557
+ : null
558
+ if (ref == null) {
559
+ return false
560
+ }
561
+ ref.value = value
562
+ return true
563
+ }
564
+
565
+ function escapeRegExp(ch: string): string {
566
+ return ch.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
497
567
  }
498
568
 
499
569
  export function Sscanln(
@@ -25,6 +25,6 @@ describe('util/conc override', () => {
25
25
 
26
26
  release.close()
27
27
  expect(await q.WaitIdle(Background(), null)).toBeNull()
28
- expect(done).toEqual([0, 1, 2, 3, 4])
28
+ expect([...done].sort((a, b) => a - b)).toEqual([0, 1, 2, 3, 4])
29
29
  })
30
30
  })
@@ -0,0 +1,64 @@
1
+ import * as $ from '@goscript/builtin/index.js'
2
+ import { LimitedReader, MultiWriter, TeeReader } from './index.js'
3
+ import { describe, expect, test } from 'vitest'
4
+
5
+ class sliceReader {
6
+ constructor(private data: Uint8Array) {}
7
+
8
+ Read(p: $.Bytes): [number, $.GoError] {
9
+ const n = Math.min($.len(p), this.data.length)
10
+ p!.set(this.data.subarray(0, n), 0)
11
+ this.data = this.data.subarray(n)
12
+ return [n, n === 0 ? new Error('EOF') as $.GoError : null]
13
+ }
14
+ }
15
+
16
+ class captureWriter {
17
+ public chunks: number[] = []
18
+
19
+ Write(p: $.Bytes): [number, $.GoError] {
20
+ this.chunks.push(...Array.from(p ?? []))
21
+ return [$.len(p), null]
22
+ }
23
+ }
24
+
25
+ describe('io override', () => {
26
+ test('LimitedReader accepts generated struct-literal construction', async () => {
27
+ const reader = new LimitedReader({
28
+ R: new sliceReader($.stringToBytes('abcdef')),
29
+ N: 3,
30
+ })
31
+ const buf = new Uint8Array(8)
32
+
33
+ const [n, err] = await reader.Read(buf)
34
+
35
+ expect(err).toBeNull()
36
+ expect(n).toBe(3)
37
+ expect(Buffer.from(buf.subarray(0, n)).toString('utf8')).toBe('abc')
38
+ })
39
+
40
+ test('TeeReader accepts nullable generated interface values', () => {
41
+ const writer = new captureWriter()
42
+ const reader = TeeReader(new sliceReader($.stringToBytes('abc')), writer)
43
+ const buf = new Uint8Array(4)
44
+
45
+ const [n, err] = reader.Read(buf)
46
+
47
+ expect(err).toBeNull()
48
+ expect(n).toBe(3)
49
+ expect(Buffer.from(writer.chunks).toString('utf8')).toBe('abc')
50
+ })
51
+
52
+ test('MultiWriter accepts nullable generated interface values', () => {
53
+ const first = new captureWriter()
54
+ const second = new captureWriter()
55
+ const writer = MultiWriter(first, second)
56
+
57
+ const [n, err] = writer.Write($.stringToBytes('abc'))
58
+
59
+ expect(err).toBeNull()
60
+ expect(n).toBe(3)
61
+ expect(Buffer.from(first.chunks).toString('utf8')).toBe('abc')
62
+ expect(Buffer.from(second.chunks).toString('utf8')).toBe('abc')
63
+ })
64
+ })
package/gs/io/io.ts CHANGED
@@ -232,7 +232,7 @@ class DiscardWriter implements Writer {
232
232
  }
233
233
  }
234
234
 
235
- export const Discard: Writer = new DiscardWriter()
235
+ export const Discard: Writer | null = new DiscardWriter()
236
236
 
237
237
  // WriteString writes the contents of the string s to w, which accepts a slice of bytes
238
238
  export function WriteString(w: Writer, s: string): [number, $.GoError] {
@@ -248,12 +248,18 @@ export function WriteString(w: Writer, s: string): [number, $.GoError] {
248
248
 
249
249
  // LimitedReader reads from R but limits the amount of data returned to just N bytes
250
250
  export class LimitedReader implements Reader {
251
- public R: Reader
251
+ public R: Reader | null
252
252
  public N: number
253
253
 
254
- constructor(r: Reader, n: number) {
255
- this.R = r
256
- this.N = n
254
+ constructor(r?: Reader | { R?: Reader | null; N?: number } | null, n?: number) {
255
+ if (r != null && typeof (r as { Read?: unknown }).Read !== 'function') {
256
+ const init = r as { R?: Reader | null; N?: number }
257
+ this.R = init.R ?? null
258
+ this.N = init.N ?? 0
259
+ return
260
+ }
261
+ this.R = (r as Reader | null | undefined) ?? null
262
+ this.N = n ?? 0
257
263
  }
258
264
 
259
265
  Read(p: $.Bytes): [number, $.GoError] {
@@ -261,6 +267,9 @@ export class LimitedReader implements Reader {
261
267
  if (this.N <= 0) {
262
268
  return [0, EOF]
263
269
  }
270
+ if (this.R == null) {
271
+ throw new Error('io.LimitedReader: nil reader')
272
+ }
264
273
 
265
274
  let readBuf = p
266
275
  if ($.len(p) > this.N) {
@@ -645,19 +654,22 @@ class multiReader implements Reader {
645
654
  }
646
655
 
647
656
  // MultiWriter creates a writer that duplicates its writes to all the provided writers
648
- export function MultiWriter(...writers: Writer[]): Writer {
657
+ export function MultiWriter(...writers: (Writer | null)[]): Writer {
649
658
  return new multiWriter(writers.slice())
650
659
  }
651
660
 
652
661
  class multiWriter implements Writer {
653
- private writers: Writer[]
662
+ private writers: (Writer | null)[]
654
663
 
655
- constructor(writers: Writer[]) {
664
+ constructor(writers: (Writer | null)[]) {
656
665
  this.writers = writers
657
666
  }
658
667
 
659
668
  Write(p: $.Bytes): [number, $.GoError] {
660
669
  for (const w of this.writers) {
670
+ if (w == null) {
671
+ throw new Error('io.MultiWriter: nil writer')
672
+ }
661
673
  const [n, err] = w.Write(p)
662
674
  if (err !== null) {
663
675
  return [n, err]
@@ -671,22 +683,28 @@ class multiWriter implements Writer {
671
683
  }
672
684
 
673
685
  // TeeReader returns a Reader that writes to w what it reads from r
674
- export function TeeReader(r: Reader, w: Writer): Reader {
686
+ export function TeeReader(r: Reader | null, w: Writer | null): Reader {
675
687
  return new teeReader(r, w)
676
688
  }
677
689
 
678
690
  class teeReader implements Reader {
679
- private r: Reader
680
- private w: Writer
691
+ private r: Reader | null
692
+ private w: Writer | null
681
693
 
682
- constructor(r: Reader, w: Writer) {
694
+ constructor(r: Reader | null, w: Writer | null) {
683
695
  this.r = r
684
696
  this.w = w
685
697
  }
686
698
 
687
699
  Read(p: $.Bytes): [number, $.GoError] {
700
+ if (this.r == null) {
701
+ throw new Error('io.TeeReader: nil reader')
702
+ }
688
703
  const [n, err] = this.r.Read(p)
689
704
  if (n > 0) {
705
+ if (this.w == null) {
706
+ throw new Error('io.TeeReader: nil writer')
707
+ }
690
708
  const [nw, ew] = this.w.Write($.goSlice(p, 0, n))
691
709
  if (ew !== null) {
692
710
  return [n, ew]
@@ -2,7 +2,14 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import * as $ from '@goscript/builtin/index.js'
4
4
 
5
- import { Handler, Header_Get, Header_Set, MethodGet, NewRequest, StatusPartialContent } from '../index.js'
5
+ import {
6
+ Handler,
7
+ Header_Get,
8
+ Header_Set,
9
+ MethodGet,
10
+ NewRequest,
11
+ StatusPartialContent,
12
+ } from '../index.js'
6
13
  import { NewServer, NewUnstartedServer, Server_Start } from './index.js'
7
14
 
8
15
  describe('net/http/httptest override', () => {
@@ -12,12 +19,13 @@ describe('net/http/httptest override', () => {
12
19
  }
13
20
 
14
21
  const srv = NewServer(handler)
15
- expect(srv.URL).toBe('http://127.0.0.1')
22
+ expect(srv.URL).toMatch(/^http:\/\/goscript-httptest-\d+\.invalid$/)
16
23
  expect(srv.Client()).toBeTruthy()
17
24
  expect(srv.Config().Handler).toBe(handler)
18
25
  expect(Server_Start(NewUnstartedServer(handler))?.Error()).toBe(
19
26
  'net/http/httptest: Server.Start is not implemented in GoScript',
20
27
  )
28
+ srv.Close()
21
29
  })
22
30
 
23
31
  it('routes Client.Do through the in-memory server handler', async () => {
@@ -49,5 +57,29 @@ describe('net/http/httptest override', () => {
49
57
  expect(n).toBe(4)
50
58
  expect(Buffer.from(buf).toString('utf8')).toBe('data')
51
59
  expect(resp!.Body!.Close()).toBeNull()
60
+ srv.Close()
61
+ })
62
+
63
+ it('awaits async handlers through Server.Client transport', async () => {
64
+ const srv = NewServer({
65
+ async ServeHTTP(w) {
66
+ await Promise.resolve()
67
+ w!.WriteHeader(StatusPartialContent)
68
+ w!.Write($.stringToBytes('async'))
69
+ },
70
+ })
71
+ const [req, reqErr] = NewRequest(MethodGet, srv.URL + '/async', null)
72
+ expect(reqErr).toBeNull()
73
+
74
+ const [resp, err] = await srv.Client().Do(req)
75
+
76
+ expect(err).toBeNull()
77
+ expect(resp?.StatusCode).toBe(StatusPartialContent)
78
+ const buf = new Uint8Array(5)
79
+ const [n, readErr] = resp!.Body!.Read(buf)
80
+ expect(readErr).toBeNull()
81
+ expect(n).toBe(5)
82
+ expect(Buffer.from(buf).toString('utf8')).toBe('async')
83
+ srv.Close()
52
84
  })
53
85
  })
@@ -58,10 +58,16 @@ export function NewRecorder(): ResponseRecorder {
58
58
  return new ResponseRecorder()
59
59
  }
60
60
 
61
- export function NewRequest(method: string, target: string, body: io.Reader | null): http.Request {
61
+ export function NewRequest(
62
+ method: string,
63
+ target: string,
64
+ body: io.Reader | null,
65
+ ): http.Request {
62
66
  const [req, err] = http.NewRequest(method, target, body)
63
67
  if (err != null || req == null) {
64
- throw err ?? errors.New('net/http/httptest: NewRequest returned nil request')
68
+ throw (
69
+ err ?? errors.New('net/http/httptest: NewRequest returned nil request')
70
+ )
65
71
  }
66
72
  return req
67
73
  }
@@ -71,21 +77,26 @@ export class Server {
71
77
  private handler: http.Handler | null
72
78
 
73
79
  constructor(init?: Partial<Server> & { Handler?: http.Handler | null }) {
74
- this.URL = init?.URL ?? 'http://127.0.0.1'
75
80
  this.handler = init?.Handler ?? null
81
+ this.URL = init?.URL ?? http.RegisterInProcessServer(this.handler)
76
82
  }
77
83
 
78
84
  public Client(): http.Client {
79
85
  return new http.Client({ Transport: new serverTransport(this) })
80
86
  }
81
87
 
82
- public Close(): void {}
88
+ public Close(): void {
89
+ http.UnregisterInProcessServer(this.URL)
90
+ }
83
91
 
84
92
  public Config(): http.Server {
85
93
  return new http.Server({ Handler: this.handler })
86
94
  }
87
95
 
88
- public ServeHTTP(w: http.ResponseWriter | null, r: http.Request | $.VarRef<http.Request> | null): void | Promise<void> {
96
+ public ServeHTTP(
97
+ w: http.ResponseWriter | null,
98
+ r: http.Request | $.VarRef<http.Request> | null,
99
+ ): void | Promise<void> {
89
100
  return this.handler?.ServeHTTP(w, r)
90
101
  }
91
102
  }
@@ -93,7 +104,9 @@ export class Server {
93
104
  class serverTransport implements http.RoundTripper {
94
105
  constructor(private server: Server) {}
95
106
 
96
- public RoundTrip(req: http.Request | $.VarRef<http.Request> | null): [http.Response | null, $.GoError] {
107
+ public async RoundTrip(
108
+ req: http.Request | $.VarRef<http.Request> | null,
109
+ ): Promise<[http.Response | null, $.GoError]> {
97
110
  const request = $.pointerValue<http.Request | null>(req)
98
111
  if (request == null) {
99
112
  return [null, errors.New('net/http: nil Request')]
@@ -101,7 +114,7 @@ class serverTransport implements http.RoundTripper {
101
114
  const recorder = NewRecorder()
102
115
  const served = this.server.ServeHTTP(recorder, request)
103
116
  if (served instanceof Promise) {
104
- return [null, errors.New('net/http/httptest: async handlers are not supported by Client.Do')]
117
+ await served
105
118
  }
106
119
  return [recorder.Result(), null]
107
120
  }
@@ -116,5 +129,7 @@ export function NewUnstartedServer(handler: http.Handler | null): Server {
116
129
  }
117
130
 
118
131
  export function Server_Start(_s: Server | $.VarRef<Server> | null): $.GoError {
119
- return errors.New('net/http/httptest: Server.Start is not implemented in GoScript')
132
+ return errors.New(
133
+ 'net/http/httptest: Server.Start is not implemented in GoScript',
134
+ )
120
135
  }
@@ -7,6 +7,9 @@ import {
7
7
  File,
8
8
  FileSystem,
9
9
  Header,
10
+ Header_Add,
11
+ Header_Del,
12
+ Header_Get,
10
13
  Header_Set,
11
14
  HandlerFunc_ServeHTTP,
12
15
  Get,
@@ -19,11 +22,16 @@ import {
19
22
  ResponseWriter,
20
23
  Server,
21
24
  StatusCreated,
25
+ StatusForbidden,
26
+ StatusMethodNotAllowed,
22
27
  StatusNotFound,
23
28
  StatusOK,
24
29
  StatusServiceUnavailable,
30
+ StatusTeapot,
25
31
  StatusText,
26
32
  StatusTooManyRequests,
33
+ StatusUnauthorized,
34
+ StatusUnsupportedMediaType,
27
35
  } from './index.js'
28
36
 
29
37
  describe('net/http override', () => {
@@ -33,6 +41,11 @@ describe('net/http override', () => {
33
41
  expect(resp.StatusCode).toBe(200)
34
42
  expect(StatusText(resp.StatusCode)).toBe('OK')
35
43
  expect(StatusText(StatusNotFound)).toBe('Not Found')
44
+ expect(StatusText(StatusUnauthorized)).toBe('Unauthorized')
45
+ expect(StatusText(StatusForbidden)).toBe('Forbidden')
46
+ expect(StatusText(StatusMethodNotAllowed)).toBe('Method Not Allowed')
47
+ expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
48
+ expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
36
49
  expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
37
50
  expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
38
51
  expect(StatusText(599)).toBe('')
@@ -92,6 +105,23 @@ describe('net/http override', () => {
92
105
  expect(resp?.StatusCode).toBe(StatusOK)
93
106
  })
94
107
 
108
+ it('canonicalizes header keys for case-insensitive lookup', () => {
109
+ const header = new Header()
110
+
111
+ Header_Set(header, 'Content-Type', 'application/json')
112
+
113
+ expect(header.has('Content-Type')).toBe(true)
114
+ expect(header.has('content-type')).toBe(false)
115
+ expect(Header_Get(header, 'content-type')).toBe('application/json')
116
+
117
+ Header_Add(header, 'x-pack-id', 'pack-1')
118
+ Header_Add(header, 'X-Pack-Id', 'pack-2')
119
+ expect(Array.from(header.get('X-Pack-Id') ?? [])).toEqual(['pack-1', 'pack-2'])
120
+
121
+ Header_Del(header, 'x-pack-id')
122
+ expect(Header_Get(header, 'X-Pack-ID')).toBe('')
123
+ })
124
+
95
125
  it('accepts server context and shutdown surfaces', () => {
96
126
  const srv = new Server({
97
127
  Addr: ':0',