11package scala .build .tests
22
33import com .eed3si9n .expecty .Expecty .expect
4+ import com .sun .net .httpserver .HttpServer
5+ import coursier .cache .FileCache
6+ import coursier .util .Task
47import coursier .version .Version
58
9+ import java .net .InetSocketAddress
10+ import java .util .UUID
11+ import java .util .concurrent .atomic .AtomicBoolean
12+ import java .util .concurrent .{ConcurrentLinkedQueue , Executors }
13+
614import scala .build .Ops .*
715import scala .build .Position .File
816import scala .build .actionable .ActionableDiagnostic .*
917import scala .build .actionable .ActionablePreprocessor
1018import scala .build .options .{BuildOptions , InternalOptions , SuppressWarningOptions }
1119import scala .build .{BuildThreads , Directories , LocalRepo }
20+ import scala .jdk .CollectionConverters .*
1221
1322class ActionableDiagnosticTests extends TestUtil .ScalaCliBuildSuite {
1423
@@ -23,6 +32,50 @@ class ActionableDiagnosticTests extends TestUtil.ScalaCliBuildSuite {
2332
2433 def path2url (p : os.Path ): String = p.toIO.toURI.toURL.toString
2534
35+ /** Minimal HTTP Maven repo: records every request path, then optional delay on
36+ * `maven-metadata.xml` when `delayWhen()` is true, then serves a body from `responses` or 404.
37+ */
38+ def withRecordingMavenRepo (
39+ responses : Map [String , Array [Byte ]],
40+ delayOnMetadataMs : Long = 0 ,
41+ delayWhen : () => Boolean = () => false
42+ )(body : (String , ConcurrentLinkedQueue [String ]) => Unit ): Unit =
43+ val recorded = new ConcurrentLinkedQueue [String ]()
44+ val address = " 127.0.0.1"
45+ val server = HttpServer .create(new InetSocketAddress (address, 0 ), 0 )
46+ server.setExecutor(Executors .newCachedThreadPool())
47+ server.createContext(
48+ " /" ,
49+ ex => {
50+ val path = ex.getRequestURI.getPath
51+ recorded.offer(path)
52+ if delayOnMetadataMs > 0 && delayWhen() && path.endsWith(" maven-metadata.xml" ) then
53+ Thread .sleep(delayOnMetadataMs)
54+ responses.get(path) match
55+ case Some (bytes) =>
56+ ex.getResponseHeaders.set(" Content-Type" , " application/xml" )
57+ ex.sendResponseHeaders(200 , bytes.length)
58+ ex.getResponseBody.write(bytes)
59+ ex.getResponseBody.close()
60+ case None =>
61+ ex.sendResponseHeaders(404 , - 1 )
62+ ex.close()
63+ }
64+ )
65+ server.start()
66+ try
67+ val base = s " http:// $address: ${server.getAddress.getPort}/ "
68+ body(base, recorded)
69+ finally server.stop(0 )
70+
71+ def buildOptionsWithEmptyCoursierCache (opts : BuildOptions ): BuildOptions =
72+ val dir = os.temp.dir(prefix = " scala-cli-actionable-diagnostic-coursier-" )
73+ opts.copy(internal =
74+ opts.internal.copy(
75+ cache = Some (FileCache [Task ]().withLocation(dir.toString))
76+ )
77+ )
78+
2679 test(" using outdated os-lib" ) {
2780 val dependencyOsLib = " com.lihaoyi::os-lib:0.7.8"
2881 val testInputs = TestInputs (
@@ -263,4 +316,223 @@ class ActionableDiagnosticTests extends TestUtil.ScalaCliBuildSuite {
263316 expect(testLibDiagnosticOpt.isEmpty)
264317 }
265318 }
319+
320+ test(" actionable outdated check for toolkit skips user repository metadata" ) {
321+ val meta =
322+ """ <?xml version="1.0" encoding="UTF-8"?>
323+ |<metadata>
324+ | <groupId>org.scala-lang</groupId>
325+ | <artifactId>toolkit_3</artifactId>
326+ | <versioning>
327+ | <latest>99.0.0</latest>
328+ | <release>99.0.0</release>
329+ | <versions>
330+ | <version>0.3.0</version>
331+ | <version>99.0.0</version>
332+ | </versions>
333+ | </versioning>
334+ |</metadata>
335+ |""" .stripMargin.getBytes(" UTF-8" )
336+ val responses = Map (" /org/scala-lang/toolkit_3/maven-metadata.xml" -> meta)
337+ withRecordingMavenRepo(responses)((repoUrl, recorded) =>
338+ val testInputs = TestInputs (
339+ os.rel / " Foo.scala" ->
340+ """ //> using toolkit 0.3.0
341+ |
342+ |object Hello extends App {
343+ | println("Hello")
344+ |}
345+ |""" .stripMargin
346+ )
347+ val withRepo = baseOptions.copy(
348+ classPathOptions =
349+ baseOptions.classPathOptions.copy(extraRepositories = Seq (repoUrl))
350+ )
351+ testInputs.withBuild(withRepo, buildThreads, None , actionableDiagnostics = true ) {
352+ (_, _, maybeBuild) =>
353+ val build = maybeBuild.orThrow
354+ ActionablePreprocessor
355+ .generateActionableDiagnostics(buildOptionsWithEmptyCoursierCache(build.options))
356+ .orThrow
357+ val paths = recorded.asScala.toSeq
358+ expect(! paths.exists(_.contains(" toolkit_3/maven-metadata.xml" )))
359+ }
360+ )
361+ }
362+
363+ test(" actionable outdated check for org.scala-lang skips user repository metadata" ) {
364+ val u = UUID .randomUUID().toString.replace(" -" , " " )
365+ val art = s " scala_cli_fake_ $u"
366+ val meta =
367+ s """ <?xml version="1.0" encoding="UTF-8"?>
368+ |<metadata>
369+ | <groupId>org.scala-lang</groupId>
370+ | <artifactId> ${art}_3</artifactId>
371+ | <versioning>
372+ | <latest>99.0.0</latest>
373+ | <release>99.0.0</release>
374+ | <versions>
375+ | <version>0.1.0</version>
376+ | <version>99.0.0</version>
377+ | </versions>
378+ | </versioning>
379+ |</metadata>
380+ | """ .stripMargin.getBytes(" UTF-8" )
381+ val pom =
382+ s """ <?xml version='1.0' encoding='UTF-8'?>
383+ |<project>
384+ | <groupId>org.scala-lang</groupId>
385+ | <artifactId> ${art}_3</artifactId>
386+ | <version>0.1.0</version>
387+ |</project> """ .stripMargin.getBytes(" UTF-8" )
388+ val responses = Map (
389+ s " /org/scala-lang/ ${art}_3/maven-metadata.xml " -> meta,
390+ s " /org/scala-lang/ ${art}_3/0.1.0/ ${art}_3-0.1.0.pom " -> pom
391+ )
392+ withRecordingMavenRepo(responses)((repoUrl, recorded) =>
393+ val testInputs = TestInputs (
394+ os.rel / " Foo.scala" ->
395+ s """ //> using dep org.scala-lang:: $art:0.1.0
396+ |
397+ |object Hello extends App {
398+ | println("Hello")
399+ |}
400+ | """ .stripMargin
401+ )
402+ val withRepo = baseOptions.copy(
403+ classPathOptions =
404+ baseOptions.classPathOptions.copy(extraRepositories = Seq (repoUrl))
405+ )
406+ testInputs.withBuild(withRepo, buildThreads, None , actionableDiagnostics = true ) {
407+ (_, _, maybeBuild) =>
408+ val build = maybeBuild.orThrow
409+ ActionablePreprocessor
410+ .generateActionableDiagnostics(buildOptionsWithEmptyCoursierCache(build.options))
411+ .orThrow
412+ val paths = recorded.asScala.toSeq
413+ expect(! paths.exists(p => p.contains(s " ${art}_3/ " ) && p.contains(" maven-metadata.xml" )))
414+ }
415+ )
416+ }
417+
418+ test(" actionable outdated check still consults user repository for other organizations" ) {
419+ val u = UUID .randomUUID().toString.replace(" -" , " " )
420+ val art = s " scala_cli_fake_ $u"
421+ val meta =
422+ s """ <?xml version="1.0" encoding="UTF-8"?>
423+ |<metadata>
424+ | <groupId>test-org</groupId>
425+ | <artifactId> ${art}_3</artifactId>
426+ | <versioning>
427+ | <latest>99.0.0</latest>
428+ | <release>99.0.0</release>
429+ | <versions>
430+ | <version>0.1.0</version>
431+ | <version>99.0.0</version>
432+ | </versions>
433+ | </versioning>
434+ |</metadata>
435+ | """ .stripMargin.getBytes(" UTF-8" )
436+ val pom =
437+ s """ <?xml version='1.0' encoding='UTF-8'?>
438+ |<project>
439+ | <groupId>test-org</groupId>
440+ | <artifactId> ${art}_3</artifactId>
441+ | <version>0.1.0</version>
442+ |</project> """ .stripMargin.getBytes(" UTF-8" )
443+ val responses = Map (
444+ s " /test-org/ ${art}_3/maven-metadata.xml " -> meta,
445+ s " /test-org/ ${art}_3/0.1.0/ ${art}_3-0.1.0.pom " -> pom
446+ )
447+ withRecordingMavenRepo(responses)((repoUrl, recorded) =>
448+ val testInputs = TestInputs (
449+ os.rel / " Foo.scala" ->
450+ s """ //> using dep test-org:: $art:0.1.0
451+ |
452+ |object Hello extends App {
453+ | println("Hello")
454+ |}
455+ | """ .stripMargin
456+ )
457+ val withRepo = baseOptions.copy(
458+ classPathOptions =
459+ baseOptions.classPathOptions.copy(extraRepositories = Seq (repoUrl))
460+ )
461+ testInputs.withBuild(withRepo, buildThreads, None , actionableDiagnostics = true ) {
462+ (_, _, maybeBuild) =>
463+ val build = maybeBuild.orThrow
464+ val paths = recorded.asScala.toSeq
465+ expect(paths.exists(p => p.contains(s " ${art}_3/ " ) && p.contains(" maven-metadata.xml" )))
466+ val updateDiagnostics =
467+ ActionablePreprocessor .generateActionableDiagnostics(build.options).orThrow
468+ val dOpt = updateDiagnostics.collectFirst {
469+ case diagnostic : ActionableDependencyUpdateDiagnostic => diagnostic
470+ }
471+ expect(dOpt.nonEmpty)
472+ expect(dOpt.get.newVersion == " 99.0.0" )
473+ }
474+ )
475+ }
476+
477+ test(" actionable outdated check times out slow user repository" ) {
478+ val u = UUID .randomUUID().toString.replace(" -" , " " )
479+ val art = s " scala_cli_fake_ $u"
480+ val meta =
481+ s """ <?xml version="1.0" encoding="UTF-8"?>
482+ |<metadata>
483+ | <groupId>test-org</groupId>
484+ | <artifactId> ${art}_3</artifactId>
485+ | <versioning>
486+ | <latest>0.2.0</latest>
487+ | <release>0.2.0</release>
488+ | <versions>
489+ | <version>0.1.0</version>
490+ | <version>0.2.0</version>
491+ | </versions>
492+ | </versioning>
493+ |</metadata>
494+ | """ .stripMargin.getBytes(" UTF-8" )
495+ val pom =
496+ s """ <?xml version='1.0' encoding='UTF-8'?>
497+ |<project>
498+ | <groupId>test-org</groupId>
499+ | <artifactId> ${art}_3</artifactId>
500+ | <version>0.1.0</version>
501+ |</project> """ .stripMargin.getBytes(" UTF-8" )
502+ val responses = Map (
503+ s " /test-org/ ${art}_3/maven-metadata.xml " -> meta,
504+ s " /test-org/ ${art}_3/0.1.0/ ${art}_3-0.1.0.pom " -> pom
505+ )
506+ val slowAfterClear = new AtomicBoolean (false )
507+ withRecordingMavenRepo(
508+ responses,
509+ delayOnMetadataMs = 30_000L ,
510+ delayWhen = () => slowAfterClear.get()
511+ )((repoUrl, _) =>
512+ val testInputs = TestInputs (
513+ os.rel / " Foo.scala" ->
514+ s """ //> using dep test-org:: $art:0.1.0
515+ |
516+ |object Hello extends App {
517+ | println("Hello")
518+ |}
519+ | """ .stripMargin
520+ )
521+ val withRepo = baseOptions.copy(
522+ classPathOptions =
523+ baseOptions.classPathOptions.copy(extraRepositories = Seq (repoUrl))
524+ )
525+ testInputs.withBuild(withRepo, buildThreads, None , actionableDiagnostics = true ) {
526+ (_, _, maybeBuild) =>
527+ val build = maybeBuild.orThrow
528+ slowAfterClear.set(true )
529+ val t0 = System .nanoTime()
530+ ActionablePreprocessor
531+ .generateActionableDiagnostics(buildOptionsWithEmptyCoursierCache(build.options))
532+ .orThrow
533+ val elapsedMs = (System .nanoTime() - t0) / 1_000_000
534+ expect(elapsedMs < 15_000 )
535+ }
536+ )
537+ }
266538}
0 commit comments