@voxgig/sdkgen 4.24.0 → 4.24.2

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 (29) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/Deploy.js +9 -6
  3. package/dist/cmp/Deploy.js.map +1 -1
  4. package/dist/helpers/semver.js +3 -1
  5. package/dist/helpers/semver.js.map +1 -1
  6. package/dist/tsconfig.tsbuildinfo +1 -1
  7. package/package.json +4 -4
  8. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  9. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  10. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  11. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  12. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  13. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  14. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  15. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  16. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  17. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  18. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  19. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  20. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  21. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  22. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  23. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  24. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  25. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  26. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  27. package/project/sdkgen-package.json +1 -1
  28. package/src/cmp/Deploy.ts +11 -6
  29. package/src/helpers/semver.ts +3 -1
@@ -323,45 +323,111 @@ describe('pipeline:feature order', () => {
323
323
  })
324
324
 
325
325
 
326
+ // A cookie credential as prepareAuth writes it: `<scheme>=K` for the probe
327
+ // key, with no scheme prefix and nothing else in the bag.
328
+ const COOKIE_PAIR = /^[^=;]+=K$/
329
+
330
+
326
331
  describe('pipeline:prepareAuth', () => {
327
332
 
328
333
  // Fake client so the exact options.auth / apikey shape is controlled.
329
- function authCtx(options, headers) {
330
- return base({ client: { options: () => options }, spec: headers == null ? null : { headers } })
334
+ function authCtx(options, spec) {
335
+ return base({ client: { options: () => options }, spec })
336
+ }
337
+
338
+ function bags() {
339
+ return { headers: {}, query: {} }
340
+ }
341
+
342
+ // `basic: false` is explicit: an HTTP Basic API's generated config carries
343
+ // `auth.basic: true`, and a client that merges it in takes a branch that
344
+ // needs a secret as well. With none supplied that branch deliberately
345
+ // writes nothing, which the probe below would read as a public API.
346
+ function auth(prefix) {
347
+ return { prefix, basic: false }
348
+ }
349
+
350
+ // Run prepareAuth with both containers present and see which one the
351
+ // generated utility writes to, and under what name. Null means this SDK
352
+ // places no credential at all — a public API — which is a legitimate
353
+ // shape, and the tests below assert exactly that instead.
354
+ //
355
+ // `pair` is the `<scheme>=` lead-in of a COOKIE credential, which rides the
356
+ // header bag under the key `cookie` instead of taking a header of its own.
357
+ function probe(options) {
358
+ const ctx = authCtx(options, bags())
359
+ stdutil.prepareAuth(ctx)
360
+ for (const where of ['headers', 'query']) {
361
+ const name = Object.keys(ctx.spec[where])[0]
362
+ if (null == name) continue
363
+ const value = ctx.spec[where][name]
364
+ const pair = 'headers' === where && 'cookie' === name
365
+ && COOKIE_PAIR.test(String(value))
366
+ ? String(value).slice(0, -1)
367
+ : ''
368
+ return { where, name, value, pair }
369
+ }
370
+ return null
371
+ }
372
+
373
+ const CRED = probe({ apikey: 'K', auth: auth('Bearer') })
374
+
375
+ // Every credential this SDK could possibly place: both credentials and
376
+ // Basic switched on, so whichever branch the API has, something lands
377
+ // unless the API is public.
378
+ const ANY = probe({ apikey: 'K', secret: 'S', auth: { prefix: 'Bearer', basic: true } })
379
+
380
+ function placed(options, seed) {
381
+ const spec = bags()
382
+ if (null != CRED && null != seed) spec[CRED.where][CRED.name] = seed
383
+ const ctx = authCtx(options, spec)
384
+ stdutil.prepareAuth(ctx)
385
+ return null == CRED ? undefined : ctx.spec[CRED.where][CRED.name]
331
386
  }
332
387
 
333
388
  test('guards a missing spec', () => {
334
- strictEqual(stdutil.prepareAuth(authCtx({ auth: { prefix: '' }, apikey: 'K' }, null)).code, 'auth_no_spec')
389
+ strictEqual(stdutil.prepareAuth(authCtx({ auth: auth(''), apikey: 'K' }, null)).code, 'auth_no_spec')
335
390
  })
336
391
 
337
- test('an apikey with a prefix is space-joined', () => {
338
- const ctx = authCtx({ apikey: 'K', auth: { prefix: 'Bearer' } }, {})
339
- stdutil.prepareAuth(ctx)
340
- strictEqual(ctx.spec.headers.authorization, 'Bearer K')
392
+ // Without this the cases below cannot fail for an SDK whose credential the
393
+ // probe misses: every one of them takes the public-API path instead.
394
+ test('the probe finds the credential this SDK places', () => {
395
+ strictEqual(null != CRED, null != ANY)
396
+ })
397
+
398
+ test('the apikey is placed where this API puts it', () => {
399
+ if (null == CRED) {
400
+ // A public API places nothing, and that is the whole assertion.
401
+ strictEqual(placed({ apikey: 'K', auth: auth('Bearer') }), undefined)
402
+ return
403
+ }
404
+ ok('headers' === CRED.where || 'query' === CRED.where)
405
+ if ('' !== CRED.pair) {
406
+ // A cookie credential is a `<scheme>=<key>` pair, and the scheme name
407
+ // leaves no room for the option's prefix.
408
+ ok(COOKIE_PAIR.test(String(CRED.value)), String(CRED.value))
409
+ return
410
+ }
411
+ // A header credential is prefix-joined; a query credential is the raw
412
+ // key, because a query parameter has nowhere to put a scheme name.
413
+ strictEqual(CRED.value, 'headers' === CRED.where ? 'Bearer K' : 'K')
341
414
  })
342
415
 
343
416
  test('a raw apikey (empty prefix) goes in as-is', () => {
344
- const ctx = authCtx({ apikey: 'K', auth: { prefix: '' } }, {})
345
- stdutil.prepareAuth(ctx)
346
- strictEqual(ctx.spec.headers.authorization, 'K')
417
+ const raw = null == CRED ? undefined : CRED.pair + 'K'
418
+ strictEqual(placed({ apikey: 'K', auth: auth('') }), raw)
347
419
  })
348
420
 
349
- test('an empty apikey drops the header', () => {
350
- const ctx = authCtx({ apikey: '', auth: { prefix: 'Bearer' } }, { authorization: 'stale' })
351
- stdutil.prepareAuth(ctx)
352
- strictEqual(ctx.spec.headers.authorization, undefined)
421
+ test('an empty apikey drops the credential', () => {
422
+ strictEqual(placed({ apikey: '', auth: auth('Bearer') }, 'stale'), undefined)
353
423
  })
354
424
 
355
- test('a public API (no auth block) drops the header', () => {
356
- const ctx = authCtx({ apikey: 'K' }, { authorization: 'stale' })
357
- stdutil.prepareAuth(ctx)
358
- strictEqual(ctx.spec.headers.authorization, undefined)
425
+ test('a public API (no auth block) drops the credential', () => {
426
+ strictEqual(placed({ apikey: 'K' }, 'stale'), undefined)
359
427
  })
360
428
 
361
- test('a missing apikey option drops the header', () => {
362
- const ctx = authCtx({ auth: { prefix: 'Bearer' } }, { authorization: 'stale' })
363
- stdutil.prepareAuth(ctx)
364
- strictEqual(ctx.spec.headers.authorization, undefined)
429
+ test('a missing apikey option drops the credential', () => {
430
+ strictEqual(placed({ auth: auth('Bearer') }, 'stale'), undefined)
365
431
  })
366
432
  })
367
433
 
@@ -79,10 +79,56 @@ describe('PrimaryUtility', async () => {
79
79
  })
80
80
 
81
81
 
82
+ function credential() {
83
+ const ctx = {
84
+ utility,
85
+ // `basic: false`: a Basic API's config defaults it true, and the
86
+ // branch it selects needs a secret, so the probe would find nothing.
87
+ client: { options: () => ({ apikey: 'PROBE', auth: { prefix: '', basic: false } }) },
88
+ spec: { headers: {}, query: {} },
89
+ error: (code, msg) => Object.assign(new Error(msg), { code }),
90
+ }
91
+ try { utility.prepareAuth(ctx) } catch (e) { return null }
92
+ for (const where of ['headers', 'query']) {
93
+ const name = Object.keys(ctx.spec[where] || {})[0]
94
+ if (null != name) return { where, name }
95
+ }
96
+ return null
97
+ }
98
+
99
+ // Rename the corpus's `headers` bag to the real container, and the
100
+ // `authorization` key inside it to the real credential name. Applied only
101
+ // to the prepareAuth section, so real header assertions elsewhere are
102
+ // untouched.
103
+ function retarget(node, cred) {
104
+ if (null == node || 'object' !== typeof node) return node
105
+ if (Array.isArray(node)) return node.map((n) => retarget(n, cred))
106
+ const out = {}
107
+ for (const key of Object.keys(node)) {
108
+ if ('headers' === key) {
109
+ const bag = {}
110
+ for (const bk of Object.keys(node[key] || {})) {
111
+ bag['authorization' === bk ? cred.name : bk] = retarget(node[key][bk], cred)
112
+ }
113
+ out[cred.where] = bag
114
+ }
115
+ else out[key] = retarget(node[key], cred)
116
+ }
117
+ return out
118
+ }
119
+
82
120
  test('auth-basic', async () => {
83
121
  const sdkopts = spec.prepareAuth?.DEF?.setup?.a || {}
84
122
  const authClient = SDK.test({}, sdkopts)
85
- await runset(spec.prepareAuth.basic, (ctx) => {
123
+
124
+ const cred = credential()
125
+ ok(null != cred, 'prepareAuth placed no credential in headers or query')
126
+
127
+ const cases = 'headers' === cred.where && 'authorization' === cred.name
128
+ ? spec.prepareAuth.basic
129
+ : retarget(spec.prepareAuth.basic, cred)
130
+
131
+ await runset(cases, (ctx) => {
86
132
  ctx.client = authClient
87
133
  fixctx(ctx)
88
134
  return utility.prepareAuth(ctx)
@@ -403,7 +403,14 @@ end
403
403
  --- omni's value model), which keeps smoke tests free of fixture files.
404
404
  function M.makeRunner(testfile, client)
405
405
  local specref = testfile
406
- if "string" == type(testfile) and "/" ~= testfile:sub(1, 1) then
406
+ -- A drive letter and a UNC prefix are absolute too, and neither starts
407
+ -- with "/": treating one as relative joins it onto _test_dir and reads
408
+ -- nothing. Lua has no path library, so the shapes are spelled out.
409
+ local function isabs(p)
410
+ return nil ~= p:match("^/") or nil ~= p:match("^%a:[/\\]")
411
+ or nil ~= p:match("^\\\\")
412
+ end
413
+ if "string" == type(testfile) and not isabs(testfile) then
407
414
  specref = _test_dir .. testfile
408
415
  end
409
416
 
@@ -536,58 +536,137 @@ describe("pipeline", function()
536
536
 
537
537
  describe("prepareAuth", function()
538
538
 
539
+ -- A cookie credential as prepare_auth writes it: `<scheme>=K` for the
540
+ -- probe key, with no scheme prefix and nothing else in the bag.
541
+ local function is_cookie_pair(value)
542
+ return "string" == type(value) and nil ~= value:match("^[^=;]+=K$")
543
+ end
544
+
539
545
  -- Fake client so the exact options.auth / apikey shape is controlled.
540
- local function auth_ctx(options, headers)
546
+ local function auth_ctx(options, spec)
541
547
  local ctx = base_ctx({})
542
548
  ctx.client = {
543
549
  options_map = function(_self)
544
550
  return options
545
551
  end,
546
552
  }
547
- if headers == nil then
548
- ctx.spec = nil
549
- else
550
- ctx.spec = Spec.new({ headers = headers })
551
- end
553
+ ctx.spec = spec
552
554
  return ctx
553
555
  end
554
556
 
557
+ local function auth_bags()
558
+ return Spec.new({ headers = {}, query = {} })
559
+ end
560
+
561
+ -- A cookie credential rides the header bag, because a cookie IS a header.
562
+ local function auth_bag(spec, where)
563
+ if "query" == where then return spec.query end
564
+ return spec.headers
565
+ end
566
+
567
+ -- `basic = false` is explicit: an HTTP Basic API's generated config
568
+ -- carries `auth.basic: true`, and a client that merges it in takes a
569
+ -- branch needing a secret as well. With none supplied that branch writes
570
+ -- nothing, which the probe would read as a public API.
571
+ local function auth_block(prefix)
572
+ return { prefix = prefix, basic = false }
573
+ end
574
+
575
+ -- Run prepare_auth with both containers present and see which one the
576
+ -- generated utility writes to, and under what name. nil means this SDK
577
+ -- places no credential at all - a public API - which is a legitimate
578
+ -- shape, and the tests below assert exactly that instead. `pair` is the
579
+ -- `<scheme>=` lead-in of a COOKIE credential, which rides the header bag
580
+ -- under the key "cookie" instead of taking a header of its own.
581
+ local function auth_probe(options)
582
+ local ctx = auth_ctx(options, auth_bags())
583
+ ctx.utility.prepare_auth(ctx)
584
+ for _, where in ipairs({ "headers", "query" }) do
585
+ for name, value in pairs(auth_bag(ctx.spec, where)) do
586
+ local pair = ""
587
+ if "headers" == where and "cookie" == name and is_cookie_pair(value) then
588
+ pair = value:sub(1, -2)
589
+ end
590
+ return { where = where, name = name, value = value, pair = pair }
591
+ end
592
+ end
593
+ return nil
594
+ end
595
+
596
+ local function auth_credential()
597
+ return auth_probe({ apikey = "K", auth = auth_block("Bearer") })
598
+ end
599
+
600
+ -- Every credential this SDK could possibly place: both credentials and
601
+ -- Basic switched on, so whichever branch the API has, something lands
602
+ -- unless the API is public.
603
+ local function auth_any_credential()
604
+ return auth_probe({ apikey = "K", secret = "S",
605
+ auth = { prefix = "Bearer", basic = true } })
606
+ end
607
+
608
+ local function auth_placed(options, seed)
609
+ local cred = auth_credential()
610
+ local spec = auth_bags()
611
+ if nil ~= cred and nil ~= seed then
612
+ auth_bag(spec, cred.where)[cred.name] = seed
613
+ end
614
+ local ctx = auth_ctx(options, spec)
615
+ ctx.utility.prepare_auth(ctx)
616
+ if nil == cred then return nil end
617
+ return auth_bag(ctx.spec, cred.where)[cred.name]
618
+ end
619
+
555
620
  it("guards a missing spec", function()
556
- local ctx = auth_ctx({ auth = { prefix = "" }, apikey = "K" }, nil)
621
+ local ctx = auth_ctx({ auth = auth_block(""), apikey = "K" }, nil)
557
622
  local _, err = ctx.utility.prepare_auth(ctx)
558
623
  assert.are.equal("auth_no_spec", err.code)
559
624
  end)
560
625
 
561
- it("an apikey with a prefix is space-joined", function()
562
- local ctx = auth_ctx({ apikey = "K", auth = { prefix = "Bearer" } }, {})
563
- ctx.utility.prepare_auth(ctx)
564
- assert.are.equal("Bearer K", ctx.spec.headers["authorization"])
626
+ -- Without this the cases below cannot fail for an SDK whose credential the
627
+ -- probe misses: every one of them takes the public-API path instead.
628
+ it("the probe finds the credential this SDK places", function()
629
+ assert.are.equal(nil == auth_credential(), nil == auth_any_credential())
630
+ end)
631
+
632
+ it("the apikey is placed where this API puts it", function()
633
+ local cred = auth_credential()
634
+ if nil == cred then
635
+ -- A public API places nothing, and that is the whole assertion.
636
+ assert.is_nil(auth_placed({ apikey = "K", auth = auth_block("Bearer") }))
637
+ return
638
+ end
639
+ assert.is_true("headers" == cred.where or "query" == cred.where)
640
+ if "" ~= cred.pair then
641
+ -- A cookie credential is a `<scheme>=<key>` pair, and the scheme name
642
+ -- leaves no room for the option's prefix.
643
+ assert.is_true(is_cookie_pair(cred.value), tostring(cred.value))
644
+ return
645
+ end
646
+ -- A header credential is prefix-joined; a query credential is the raw
647
+ -- key, because a query parameter has nowhere to put a scheme name.
648
+ local want = "Bearer K"
649
+ if "query" == cred.where then want = "K" end
650
+ assert.are.equal(want, cred.value)
565
651
  end)
566
652
 
567
653
  it("a raw apikey (empty prefix) goes in as-is", function()
568
- local ctx = auth_ctx({ apikey = "K", auth = { prefix = "" } }, {})
569
- ctx.utility.prepare_auth(ctx)
570
- assert.are.equal("K", ctx.spec.headers["authorization"])
654
+ local cred = auth_credential()
655
+ local want = nil
656
+ if nil ~= cred then want = cred.pair .. "K" end
657
+ assert.are.equal(want, auth_placed({ apikey = "K", auth = auth_block("") }))
571
658
  end)
572
659
 
573
- it("an empty apikey drops the header", function()
574
- local ctx = auth_ctx({ apikey = "", auth = { prefix = "Bearer" } },
575
- { authorization = "stale" })
576
- ctx.utility.prepare_auth(ctx)
577
- assert.is_nil(ctx.spec.headers["authorization"])
660
+ it("an empty apikey drops the credential", function()
661
+ assert.is_nil(auth_placed({ apikey = "", auth = auth_block("Bearer") }, "stale"))
578
662
  end)
579
663
 
580
- it("a public API (no auth block) drops the header", function()
581
- local ctx = auth_ctx({ apikey = "K" }, { authorization = "stale" })
582
- ctx.utility.prepare_auth(ctx)
583
- assert.is_nil(ctx.spec.headers["authorization"])
664
+ it("a public API (no auth block) drops the credential", function()
665
+ assert.is_nil(auth_placed({ apikey = "K" }, "stale"))
584
666
  end)
585
667
 
586
- it("a missing apikey option drops the header", function()
587
- local ctx = auth_ctx({ auth = { prefix = "Bearer" } },
588
- { authorization = "stale" })
589
- ctx.utility.prepare_auth(ctx)
590
- assert.is_nil(ctx.spec.headers["authorization"])
668
+ it("a missing apikey option drops the credential", function()
669
+ assert.is_nil(auth_placed({ auth = auth_block("Bearer") }, "stale"))
591
670
  end)
592
671
  end)
593
672
 
@@ -70,6 +70,64 @@ local function get_spec(base, ...)
70
70
  end
71
71
 
72
72
 
73
+ -- Where this SDK's prepare_auth actually puts the credential: the name is
74
+ -- the API's OWN scheme name, not always `authorization`, so it is discovered
75
+ -- by running prepare_auth once.
76
+ local function auth_credential(auth_client, auth_utility)
77
+ local ctx = auth_utility.make_context({
78
+ opname = "load",
79
+ client = auth_client,
80
+ utility = auth_utility,
81
+ }, auth_client:get_root_ctx())
82
+ ctx.spec = Spec.new({ headers = {}, query = {} })
83
+ local _, err = auth_utility.prepare_auth(ctx)
84
+ if err ~= nil then
85
+ return nil
86
+ end
87
+ for _, where in ipairs({ "headers", "query" }) do
88
+ -- A cookie credential rides the header bag, because a cookie IS a header.
89
+ local bag = ctx.spec.headers
90
+ if "query" == where then bag = ctx.spec.query end
91
+ for name, _value in pairs(bag) do
92
+ return { where = where, name = name }
93
+ end
94
+ end
95
+ return nil
96
+ end
97
+
98
+
99
+ -- Rename the corpus's `headers` bag to the real container, and the
100
+ -- `authorization` key inside it to the real credential name. Applied only to
101
+ -- the prepareAuth section, so real header assertions elsewhere are untouched.
102
+ local function retarget_auth(node, cred)
103
+ if type(node) ~= "table" then
104
+ return node
105
+ end
106
+
107
+ -- The metatable carries the corpus's `__jsontype` marker, which the struct
108
+ -- library's islist/ismap read to tell a one-element list from a map. A
109
+ -- plain-table copy leaves the rebuilt `set` unrecognisable.
110
+ local out = setmetatable({}, getmetatable(node))
111
+ for key, value in pairs(node) do
112
+ if "headers" == key then
113
+ local bag = {}
114
+ if type(value) == "table" then
115
+ setmetatable(bag, getmetatable(value))
116
+ for bagkey, bagvalue in pairs(value) do
117
+ local name = bagkey
118
+ if "authorization" == bagkey then name = cred.name end
119
+ bag[name] = retarget_auth(bagvalue, cred)
120
+ end
121
+ end
122
+ out[cred.where] = bag
123
+ else
124
+ out[key] = retarget_auth(value, cred)
125
+ end
126
+ end
127
+ return out
128
+ end
129
+
130
+
73
131
  -- Sections deliberately left empty in the shared corpus
74
132
  -- (.sdk/test/primary/<name>.aon carries a PENDING header). Everything else
75
133
  -- MUST contribute cases.
@@ -749,7 +807,23 @@ describe("PrimaryUtility", function()
749
807
  local auth_client = sdk.test(nil, setup_opts)
750
808
  local auth_utility = auth_client:get_utility()
751
809
 
752
- runsection("prepareAuth", function(cin)
810
+ -- The corpus writes the credential as `headers.authorization`: a
811
+ -- PLACEHOLDER each runner points at the container and name this API
812
+ -- actually uses.
813
+ local cred = auth_credential(auth_client, auth_utility)
814
+ assert(cred ~= nil, "prepare_auth placed no credential in headers or query")
815
+
816
+ -- An absent section is runsection's report to make.
817
+ local section = get_spec(spec, "prepareAuth")
818
+ local original = nil
819
+ if section ~= nil
820
+ and not ("headers" == cred.where and "authorization" == cred.name)
821
+ then
822
+ original = section.basic
823
+ section.basic = retarget_auth(original, cred)
824
+ end
825
+
826
+ local subject = function(cin)
753
827
  if type(cin) ~= "table" then
754
828
  cin = {}
755
829
  end
@@ -758,15 +832,21 @@ describe("PrimaryUtility", function()
758
832
 
759
833
  unwrap(utility.prepare_auth(ctx))
760
834
 
761
- -- Write live-context spec headers back for match checking
835
+ -- Write the live-context spec bags back for match checking. QUERY as
836
+ -- well as headers: this API may place its credential there.
762
837
  if ctx.spec ~= nil then
763
838
  cin["spec"] = {
764
839
  headers = ctx.spec.headers,
840
+ query = ctx.spec.query,
765
841
  }
766
842
  end
767
843
 
768
844
  return nil
769
- end)
845
+ end
846
+
847
+ local ok, failure = pcall(runsection, "prepareAuth", subject)
848
+ if original ~= nil then section.basic = original end
849
+ if not ok then error(failure) end
770
850
  end)
771
851
 
772
852
 
@@ -587,7 +587,13 @@ sub make_runner {
587
587
  my ($testfile, $client) = @_;
588
588
 
589
589
  my $specref = $testfile;
590
- if (defined $testfile && !ref $testfile && $testfile !~ m{^/}) {
590
+ # A leading slash alone is not the whole of absolute: a Windows path
591
+ # starts `C:/` or `\\host\share`, and joining one onto $DIR reads
592
+ # nothing. Spelled out rather than left to File::Spec, which an MSYS perl
593
+ # resolves to the Unix flavour and which answers no for a drive letter.
594
+ if (defined $testfile && !ref $testfile
595
+ && $testfile !~ m{^/} && $testfile !~ m{^[A-Za-z]:[\\/]}
596
+ && $testfile !~ m{^\\\\}) {
591
597
  $specref = Cwd::abs_path("$DIR/$testfile") || "$DIR/$testfile";
592
598
  }
593
599