From 72a59d932db6f16defd14bd729e0f6ec894c7e1b Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 19 May 2016 21:58:55 +0200 Subject: Rename nsc.backend.jvm.CodeGenTools to testing.BytecodeTesting --- test/junit/scala/issues/BytecodeTest.scala | 2 +- .../junit/scala/issues/OptimizedBytecodeTest.scala | 4 +- .../scala/tools/nsc/backend/jvm/BTypesTest.scala | 2 +- .../scala/tools/nsc/backend/jvm/CodeGenTools.scala | 248 --------------------- .../tools/nsc/backend/jvm/DefaultMethodTest.scala | 2 +- .../tools/nsc/backend/jvm/DirectCompileTest.scala | 2 +- .../tools/nsc/backend/jvm/IndyLambdaTest.scala | 2 +- .../tools/nsc/backend/jvm/IndySammyTest.scala | 2 +- .../tools/nsc/backend/jvm/StringConcatTest.scala | 2 +- .../jvm/analysis/NullnessAnalyzerTest.scala | 2 +- .../jvm/analysis/ProdConsAnalyzerTest.scala | 2 +- .../tools/nsc/backend/jvm/opt/AnalyzerTest.scala | 2 +- .../backend/jvm/opt/BTypesFromClassfileTest.scala | 2 +- .../tools/nsc/backend/jvm/opt/CallGraphTest.scala | 2 +- .../nsc/backend/jvm/opt/ClosureOptimizerTest.scala | 2 +- .../jvm/opt/CompactLocalVariablesTest.scala | 2 +- .../jvm/opt/EmptyExceptionHandlersTest.scala | 2 +- .../jvm/opt/EmptyLabelsAndLineNumbersTest.scala | 2 +- .../tools/nsc/backend/jvm/opt/InlineInfoTest.scala | 2 +- .../nsc/backend/jvm/opt/InlineWarningTest.scala | 2 +- .../backend/jvm/opt/InlinerIllegalAccessTest.scala | 2 +- .../jvm/opt/InlinerSeparateCompilationTest.scala | 2 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 2 +- .../nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 2 +- .../nsc/backend/jvm/opt/ScalaInlineInfoTest.scala | 2 +- .../nsc/backend/jvm/opt/SimplifyJumpsTest.scala | 2 +- .../nsc/backend/jvm/opt/UnreachableCodeTest.scala | 2 +- .../backend/jvm/opt/UnusedLocalVariablesTest.scala | 2 +- .../nsc/transform/delambdafy/DelambdafyTest.scala | 4 +- .../nsc/transform/patmat/PatmatBytecodeTest.scala | 3 +- .../scala/tools/testing/BytecodeTesting.scala | 247 ++++++++++++++++++++ 31 files changed, 277 insertions(+), 281 deletions(-) delete mode 100644 test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala create mode 100644 test/junit/scala/tools/testing/BytecodeTesting.scala (limited to 'test') diff --git a/test/junit/scala/issues/BytecodeTest.scala b/test/junit/scala/issues/BytecodeTest.scala index 7b9474b52e..8aa76bbac2 100644 --- a/test/junit/scala/issues/BytecodeTest.scala +++ b/test/junit/scala/issues/BytecodeTest.scala @@ -6,7 +6,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import scala.tools.nsc.backend.jvm.AsmUtils -import scala.tools.nsc.backend.jvm.CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import org.junit.Assert._ import scala.collection.JavaConverters._ diff --git a/test/junit/scala/issues/OptimizedBytecodeTest.scala b/test/junit/scala/issues/OptimizedBytecodeTest.scala index c69229ae22..9c0fbebde7 100644 --- a/test/junit/scala/issues/OptimizedBytecodeTest.scala +++ b/test/junit/scala/issues/OptimizedBytecodeTest.scala @@ -6,9 +6,9 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import scala.tools.nsc.backend.jvm.{AsmUtils, CodeGenTools} +import scala.tools.nsc.backend.jvm.AsmUtils -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala index e7bbbb9a4f..ebeb577149 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/BTypesTest.scala @@ -7,7 +7,7 @@ import org.junit.Test import scala.tools.asm.Opcodes import org.junit.Assert._ -import scala.tools.nsc.backend.jvm.CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.testing.ClearAfterClass @RunWith(classOf[JUnit4]) diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala deleted file mode 100644 index 389e5b2ead..0000000000 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ /dev/null @@ -1,248 +0,0 @@ -package scala.tools.nsc.backend.jvm - -import org.junit.Assert._ - -import scala.collection.mutable.ListBuffer -import scala.reflect.internal.util.BatchSourceFile -import scala.reflect.io.VirtualDirectory -import scala.tools.asm.Opcodes -import scala.tools.asm.tree.{AbstractInsnNode, ClassNode, MethodNode} -import scala.tools.cmd.CommandLineParser -import scala.tools.nsc.io.AbstractFile -import scala.tools.nsc.reporters.StoreReporter -import scala.tools.nsc.settings.MutableSettings -import scala.tools.nsc.{Settings, Global} -import scala.tools.partest.ASMConverters -import scala.collection.JavaConverters._ -import scala.tools.testing.TempDir -import AsmUtils._ - -object CodeGenTools { - import ASMConverters._ - - def genMethod( flags: Int = Opcodes.ACC_PUBLIC, - name: String = "m", - descriptor: String = "()V", - genericSignature: String = null, - throwsExceptions: Array[String] = null, - handlers: List[ExceptionHandler] = Nil, - localVars: List[LocalVariable] = Nil)(body: Instruction*): MethodNode = { - val node = new MethodNode(flags, name, descriptor, genericSignature, throwsExceptions) - applyToMethod(node, Method(body.toList, handlers, localVars)) - node - } - - def wrapInClass(method: MethodNode): ClassNode = { - val cls = new ClassNode() - cls.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "C", null, "java/lang/Object", null) - cls.methods.add(method) - cls - } - - private def resetOutput(compiler: Global): Unit = { - compiler.settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) - } - - def newCompiler(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { - val compiler = newCompilerWithoutVirtualOutdir(defaultArgs, extraArgs) - resetOutput(compiler) - compiler - } - - def newCompilerWithoutVirtualOutdir(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { - def showError(s: String) = throw new Exception(s) - val settings = new Settings(showError) - val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) - val (_, nonSettingsArgs) = settings.processArguments(args, processAll = true) - if (nonSettingsArgs.nonEmpty) showError("invalid compiler flags: " + nonSettingsArgs.mkString(" ")) - new Global(settings, new StoreReporter) - } - - def newRun(compiler: Global): compiler.Run = { - compiler.reporter.reset() - resetOutput(compiler) - new compiler.Run() - } - - def reporter(compiler: Global) = compiler.reporter.asInstanceOf[StoreReporter] - - def makeSourceFile(code: String, filename: String): BatchSourceFile = new BatchSourceFile(filename, code) - - def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { - def files(dir: AbstractFile): List[(String, Array[Byte])] = { - val res = ListBuffer.empty[(String, Array[Byte])] - for (f <- dir.iterator) { - if (!f.isDirectory) res += ((f.name, f.toByteArray)) - else if (f.name != "." && f.name != "..") res ++= files(f) - } - res.toList - } - files(outDir) - } - - def checkReport(compiler: Global, allowMessage: StoreReporter#Info => Boolean = _ => false): Unit = { - val disallowed = reporter(compiler).infos.toList.filter(!allowMessage(_)) // toList prevents an infer-non-wildcard-existential warning. - if (disallowed.nonEmpty) { - val msg = disallowed.mkString("\n") - assert(false, "The compiler issued non-allowed warnings or errors:\n" + msg) - } - } - - def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[(String, Array[Byte])] = { - val run = newRun(compiler) - run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2))) - checkReport(compiler, allowMessage) - getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) - } - - def compileTransformed(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, beforeBackend: compiler.Tree => compiler.Tree): List[(String, Array[Byte])] = { - compiler.settings.stopBefore.value = "jvm" :: Nil - val run = newRun(compiler) - import compiler._ - val scalaUnit = newCompilationUnit(scalaCode, "unitTestSource.scala") - val javaUnits = javaCode.map(p => newCompilationUnit(p._1, p._2)) - val units = scalaUnit :: javaUnits - run.compileUnits(units, run.parserPhase) - compiler.settings.stopBefore.value = Nil - scalaUnit.body = beforeBackend(scalaUnit.body) - checkReport(compiler, _ => false) - val run1 = newRun(compiler) - run1.compileUnits(units, run1.phaseNamed("jvm")) - checkReport(compiler, _ => false) - getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) - } - - /** - * Compile multiple Scala files separately into a single output directory. - * - * Note that a new compiler instance is created for compiling each file because symbols survive - * across runs. This makes separate compilation slower. - * - * The output directory is a physical directory, I have not figured out if / how it's possible to - * add a VirtualDirectory to the classpath of a compiler. - */ - def compileSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()): List[(String, Array[Byte])] = { - val outDir = AbstractFile.getDirectory(TempDir.createTempDir()) - val outDirPath = outDir.canonicalPath - val argsWithOutDir = extraArgs + s" -d $outDirPath -cp $outDirPath" - - for (code <- codes) { - val compiler = newCompilerWithoutVirtualOutdir(extraArgs = argsWithOutDir) - new compiler.Run().compileSources(List(makeSourceFile(code, "unitTestSource.scala"))) - checkReport(compiler, allowMessage) - afterEach(outDir) - } - - val classfiles = getGeneratedClassfiles(outDir) - outDir.delete() - classfiles - } - - def compileClassesSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()) = { - readAsmClasses(compileSeparately(codes, extraArgs, allowMessage, afterEach)) - } - - def readAsmClasses(classfiles: List[(String, Array[Byte])]) = { - classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name) - } - - def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { - readAsmClasses(compile(compiler)(code, javaCode, allowMessage)) - } - - def compileMethods(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[MethodNode] = { - compileClasses(compiler)(s"class C { $code }", allowMessage = allowMessage).head.methods.asScala.toList.filterNot(_.name == "") - } - - def singleMethodInstructions(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[Instruction] = { - val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) - instructionsFromMethod(m) - } - - def singleMethod(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): Method = { - val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) - convertMethod(m) - } - - def assertSameCode(method: Method, expected: List[Instruction]): Unit = assertSameCode(method.instructions.dropNonOp, expected) - def assertSameCode(actual: List[Instruction], expected: List[Instruction]): Unit = { - assert(actual === expected, s"\nExpected: $expected\nActual : $actual") - } - - def assertSameSummary(method: Method, expected: List[Any]): Unit = assertSameSummary(method.instructions, expected) - def assertSameSummary(actual: List[Instruction], expected: List[Any]): Unit = { - def expectedString = expected.map({ - case s: String => s""""$s"""" - case i: Int => opcodeToString(i, i) - }).mkString("List(", ", ", ")") - assert(actual.summary == expected, s"\nFound : ${actual.summaryText}\nExpected: $expectedString") - } - - def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) - def assertNoInvoke(ins: List[Instruction]): Unit = { - assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) - } - - def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) - def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { - assert(l.exists { - case Invoke(_, `receiver`, `method`, _, _) => true - case _ => false - }, l.stringLines) - } - - def assertDoesNotInvoke(m: Method, method: String): Unit = assertDoesNotInvoke(m.instructions, method) - def assertDoesNotInvoke(l: List[Instruction], method: String): Unit = { - assert(!l.exists { - case i: Invoke => i.name == method - case _ => false - }, l.stringLines) - } - - def assertInvokedMethods(m: Method, expected: List[String]): Unit = assertInvokedMethods(m.instructions, expected) - def assertInvokedMethods(l: List[Instruction], expected: List[String]): Unit = { - def quote(l: List[String]) = l.map(s => s""""$s"""").mkString("List(", ", ", ")") - val actual = l collect { case i: Invoke => i.owner + "." + i.name } - assert(actual == expected, s"\nFound : ${quote(actual)}\nExpected: ${quote(expected)}") - } - - def assertNoIndy(m: Method): Unit = assertNoIndy(m.instructions) - def assertNoIndy(l: List[Instruction]) = { - val indy = l collect { case i: InvokeDynamic => i } - assert(indy.isEmpty, indy) - } - - def getSingleMethod(classNode: ClassNode, name: String): Method = - convertMethod(classNode.methods.asScala.toList.find(_.name == name).get) - - def findAsmMethods(c: ClassNode, p: String => Boolean) = c.methods.iterator.asScala.filter(m => p(m.name)).toList.sortBy(_.name) - def findAsmMethod(c: ClassNode, name: String) = findAsmMethods(c, _ == name).head - - /** - * Instructions that match `query` when textified. - * If `query` starts with a `+`, the next instruction is returned. - */ - def findInstr(method: MethodNode, query: String): List[AbstractInsnNode] = { - val useNext = query(0) == '+' - val instrPart = if (useNext) query.drop(1) else query - val insns = method.instructions.iterator.asScala.filter(i => textify(i) contains instrPart).toList - if (useNext) insns.map(_.getNext) else insns - } - - def assertHandlerLabelPostions(h: ExceptionHandler, instructions: List[Instruction], startIndex: Int, endIndex: Int, handlerIndex: Int): Unit = { - val insVec = instructions.toVector - assertTrue(h.start == insVec(startIndex) && h.end == insVec(endIndex) && h.handler == insVec(handlerIndex)) - } - - import scala.language.implicitConversions - - implicit def aliveInstruction(ins: Instruction): (Instruction, Boolean) = (ins, true) - - implicit class MortalInstruction(val ins: Instruction) extends AnyVal { - def dead: (Instruction, Boolean) = (ins, false) - } - - implicit class listStringLines[T](val l: List[T]) extends AnyVal { - def stringLines = l.mkString("\n") - } -} diff --git a/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala index 7d4ae866fc..0991e5fbcf 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DefaultMethodTest.scala @@ -6,7 +6,7 @@ import org.junit.Test import scala.collection.JavaConverters import scala.tools.asm.Opcodes import scala.tools.asm.tree.ClassNode -import scala.tools.nsc.backend.jvm.CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import JavaConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala index e984b75518..ab57c5a1c5 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/DirectCompileTest.scala @@ -4,7 +4,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.asm.Opcodes._ import scala.tools.partest.ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/IndyLambdaTest.scala b/test/junit/scala/tools/nsc/backend/jvm/IndyLambdaTest.scala index b906942ffa..66054f246f 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/IndyLambdaTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/IndyLambdaTest.scala @@ -6,7 +6,7 @@ import org.junit.{Assert, Test} import scala.tools.asm.{Handle, Opcodes} import scala.tools.asm.tree.InvokeDynamicInsnNode import scala.tools.nsc.backend.jvm.AsmUtils._ -import scala.tools.nsc.backend.jvm.CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.testing.ClearAfterClass import scala.collection.JavaConverters._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/IndySammyTest.scala b/test/junit/scala/tools/nsc/backend/jvm/IndySammyTest.scala index 5c2ab6a2c7..598899c705 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/IndySammyTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/IndySammyTest.scala @@ -9,7 +9,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import scala.tools.nsc.reporters.StoreReporter -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/StringConcatTest.scala b/test/junit/scala/tools/nsc/backend/jvm/StringConcatTest.scala index fc0c96e71a..f300090268 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/StringConcatTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/StringConcatTest.scala @@ -9,7 +9,7 @@ import org.junit.Assert._ import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala index 075f42d18f..d37adb2265 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.asm.tree.{AbstractInsnNode, MethodNode} import scala.tools.nsc.backend.jvm.BTypes._ import scala.tools.partest.ASMConverters diff --git a/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala index 8d4bc19ec3..7f6aaca67c 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala @@ -11,7 +11,7 @@ import scala.tools.asm.Opcodes import scala.tools.asm.tree.AbstractInsnNode import scala.tools.partest.ASMConverters._ import scala.tools.testing.ClearAfterClass -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import AsmUtils._ @RunWith(classOf[JUnit4]) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala index 09675870f0..7f07ce51d3 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala @@ -11,7 +11,7 @@ import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis._ import scala.tools.nsc.backend.jvm.analysis.{AliasingFrame, AliasingAnalyzer} -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index aba0aab038..30d5db06dd 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -11,7 +11,7 @@ import org.junit.Assert._ import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 9a27c42cac..e29d41f061 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -15,7 +15,7 @@ import scala.tools.asm.tree.analysis._ import scala.tools.nsc.reporters.StoreReporter import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala index e8530af4e0..d143231882 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -17,7 +17,7 @@ import scala.tools.nsc.io._ import scala.tools.nsc.reporters.StoreReporter import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala index ac1b759fe2..8ee2b2aa6b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala index 6d566c722f..d9479fde1d 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyLabelsAndLineNumbersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyLabelsAndLineNumbersTest.scala index 7283e20745..a833192fb1 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyLabelsAndLineNumbersTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyLabelsAndLineNumbersTest.scala @@ -9,7 +9,7 @@ import scala.tools.asm.Opcodes._ import org.junit.Assert._ import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala index 5cb1aab4a9..dc3eede556 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.collection.generic.Clearable import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala index 6dd0a33289..428841e0e0 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala @@ -17,7 +17,7 @@ import scala.tools.nsc.io._ import scala.tools.nsc.reporters.StoreReporter import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala index ab1aef47cd..e0b1d758f7 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -11,7 +11,7 @@ import org.junit.Assert._ import scala.tools.asm.tree._ import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala index 075513a2b7..748eff88ea 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index b7641b5ec7..52ee118a94 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -12,7 +12,7 @@ import org.junit.Assert._ import scala.tools.asm.tree._ import scala.tools.nsc.reporters.StoreReporter -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala index 003b2d4880..1ceaaf7f69 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -12,7 +12,7 @@ import scala.tools.asm.tree.ClassNode import scala.tools.nsc.backend.jvm.AsmUtils._ import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala index 6cb3fd3bba..ba6bdcf658 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.asm.tree.ClassNode import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} import scala.tools.partest.ASMConverters diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala index 99acb318de..0133fc9dce 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala @@ -8,7 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 46f06d1d39..ca095b8a51 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -10,7 +10,7 @@ import org.junit.Assert._ import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala index 77e73e64b9..7ae946f581 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala @@ -9,7 +9,7 @@ import scala.tools.asm.Opcodes._ import org.junit.Assert._ import scala.collection.JavaConverters._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/nsc/transform/delambdafy/DelambdafyTest.scala b/test/junit/scala/tools/nsc/transform/delambdafy/DelambdafyTest.scala index e4bf038f32..d30f458177 100644 --- a/test/junit/scala/tools/nsc/transform/delambdafy/DelambdafyTest.scala +++ b/test/junit/scala/tools/nsc/transform/delambdafy/DelambdafyTest.scala @@ -1,9 +1,7 @@ package scala.tools.nsc.transform.delambdafy import scala.reflect.io.Path.jfile2path -import scala.tools.nsc.backend.jvm.CodeGenTools.getGeneratedClassfiles -import scala.tools.nsc.backend.jvm.CodeGenTools.makeSourceFile -import scala.tools.nsc.backend.jvm.CodeGenTools.newCompilerWithoutVirtualOutdir +import scala.tools.testing.BytecodeTesting._ import scala.tools.nsc.io.AbstractFile import scala.tools.testing.TempDir diff --git a/test/junit/scala/tools/nsc/transform/patmat/PatmatBytecodeTest.scala b/test/junit/scala/tools/nsc/transform/patmat/PatmatBytecodeTest.scala index aa83520efb..99975abc50 100644 --- a/test/junit/scala/tools/nsc/transform/patmat/PatmatBytecodeTest.scala +++ b/test/junit/scala/tools/nsc/transform/patmat/PatmatBytecodeTest.scala @@ -8,10 +8,9 @@ import scala.tools.asm.Opcodes._ import org.junit.Assert._ import scala.tools.nsc.backend.jvm.AsmUtils._ -import scala.tools.nsc.backend.jvm.CodeGenTools import scala.tools.testing.AssertUtil._ -import CodeGenTools._ +import scala.tools.testing.BytecodeTesting._ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass diff --git a/test/junit/scala/tools/testing/BytecodeTesting.scala b/test/junit/scala/tools/testing/BytecodeTesting.scala new file mode 100644 index 0000000000..21b1ce2e77 --- /dev/null +++ b/test/junit/scala/tools/testing/BytecodeTesting.scala @@ -0,0 +1,247 @@ +package scala.tools.testing + +import org.junit.Assert._ + +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.BatchSourceFile +import scala.reflect.io.VirtualDirectory +import scala.tools.asm.Opcodes +import scala.tools.asm.tree.{AbstractInsnNode, ClassNode, MethodNode} +import scala.tools.cmd.CommandLineParser +import scala.tools.nsc.io.AbstractFile +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.nsc.{Global, Settings} +import scala.tools.partest.ASMConverters +import scala.collection.JavaConverters._ +import scala.tools.nsc.backend.jvm.AsmUtils + +object BytecodeTesting { + import AsmUtils._ + import ASMConverters._ + + def genMethod( flags: Int = Opcodes.ACC_PUBLIC, + name: String = "m", + descriptor: String = "()V", + genericSignature: String = null, + throwsExceptions: Array[String] = null, + handlers: List[ExceptionHandler] = Nil, + localVars: List[LocalVariable] = Nil)(body: Instruction*): MethodNode = { + val node = new MethodNode(flags, name, descriptor, genericSignature, throwsExceptions) + applyToMethod(node, Method(body.toList, handlers, localVars)) + node + } + + def wrapInClass(method: MethodNode): ClassNode = { + val cls = new ClassNode() + cls.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "C", null, "java/lang/Object", null) + cls.methods.add(method) + cls + } + + private def resetOutput(compiler: Global): Unit = { + compiler.settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) + } + + def newCompiler(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { + val compiler = newCompilerWithoutVirtualOutdir(defaultArgs, extraArgs) + resetOutput(compiler) + compiler + } + + def newCompilerWithoutVirtualOutdir(defaultArgs: String = "-usejavacp", extraArgs: String = ""): Global = { + def showError(s: String) = throw new Exception(s) + val settings = new Settings(showError) + val args = (CommandLineParser tokenize defaultArgs) ++ (CommandLineParser tokenize extraArgs) + val (_, nonSettingsArgs) = settings.processArguments(args, processAll = true) + if (nonSettingsArgs.nonEmpty) showError("invalid compiler flags: " + nonSettingsArgs.mkString(" ")) + new Global(settings, new StoreReporter) + } + + def newRun(compiler: Global): compiler.Run = { + compiler.reporter.reset() + resetOutput(compiler) + new compiler.Run() + } + + def reporter(compiler: Global) = compiler.reporter.asInstanceOf[StoreReporter] + + def makeSourceFile(code: String, filename: String): BatchSourceFile = new BatchSourceFile(filename, code) + + def getGeneratedClassfiles(outDir: AbstractFile): List[(String, Array[Byte])] = { + def files(dir: AbstractFile): List[(String, Array[Byte])] = { + val res = ListBuffer.empty[(String, Array[Byte])] + for (f <- dir.iterator) { + if (!f.isDirectory) res += ((f.name, f.toByteArray)) + else if (f.name != "." && f.name != "..") res ++= files(f) + } + res.toList + } + files(outDir) + } + + def checkReport(compiler: Global, allowMessage: StoreReporter#Info => Boolean = _ => false): Unit = { + val disallowed = reporter(compiler).infos.toList.filter(!allowMessage(_)) // toList prevents an infer-non-wildcard-existential warning. + if (disallowed.nonEmpty) { + val msg = disallowed.mkString("\n") + assert(false, "The compiler issued non-allowed warnings or errors:\n" + msg) + } + } + + def compile(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[(String, Array[Byte])] = { + val run = newRun(compiler) + run.compileSources(makeSourceFile(scalaCode, "unitTestSource.scala") :: javaCode.map(p => makeSourceFile(p._1, p._2))) + checkReport(compiler, allowMessage) + getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) + } + + def compileTransformed(compiler: Global)(scalaCode: String, javaCode: List[(String, String)] = Nil, beforeBackend: compiler.Tree => compiler.Tree): List[(String, Array[Byte])] = { + compiler.settings.stopBefore.value = "jvm" :: Nil + val run = newRun(compiler) + import compiler._ + val scalaUnit = newCompilationUnit(scalaCode, "unitTestSource.scala") + val javaUnits = javaCode.map(p => newCompilationUnit(p._1, p._2)) + val units = scalaUnit :: javaUnits + run.compileUnits(units, run.parserPhase) + compiler.settings.stopBefore.value = Nil + scalaUnit.body = beforeBackend(scalaUnit.body) + checkReport(compiler, _ => false) + val run1 = newRun(compiler) + run1.compileUnits(units, run1.phaseNamed("jvm")) + checkReport(compiler, _ => false) + getGeneratedClassfiles(compiler.settings.outputDirs.getSingleOutput.get) + } + + /** + * Compile multiple Scala files separately into a single output directory. + * + * Note that a new compiler instance is created for compiling each file because symbols survive + * across runs. This makes separate compilation slower. + * + * The output directory is a physical directory, I have not figured out if / how it's possible to + * add a VirtualDirectory to the classpath of a compiler. + */ + def compileSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()): List[(String, Array[Byte])] = { + val outDir = AbstractFile.getDirectory(TempDir.createTempDir()) + val outDirPath = outDir.canonicalPath + val argsWithOutDir = extraArgs + s" -d $outDirPath -cp $outDirPath" + + for (code <- codes) { + val compiler = newCompilerWithoutVirtualOutdir(extraArgs = argsWithOutDir) + new compiler.Run().compileSources(List(makeSourceFile(code, "unitTestSource.scala"))) + checkReport(compiler, allowMessage) + afterEach(outDir) + } + + val classfiles = getGeneratedClassfiles(outDir) + outDir.delete() + classfiles + } + + def compileClassesSeparately(codes: List[String], extraArgs: String = "", allowMessage: StoreReporter#Info => Boolean = _ => false, afterEach: AbstractFile => Unit = _ => ()) = { + readAsmClasses(compileSeparately(codes, extraArgs, allowMessage, afterEach)) + } + + def readAsmClasses(classfiles: List[(String, Array[Byte])]) = { + classfiles.map(p => AsmUtils.readClass(p._2)).sortBy(_.name) + } + + def compileClasses(compiler: Global)(code: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + readAsmClasses(compile(compiler)(code, javaCode, allowMessage)) + } + + def compileMethods(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[MethodNode] = { + compileClasses(compiler)(s"class C { $code }", allowMessage = allowMessage).head.methods.asScala.toList.filterNot(_.name == "") + } + + def singleMethodInstructions(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[Instruction] = { + val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) + instructionsFromMethod(m) + } + + def singleMethod(compiler: Global)(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): Method = { + val List(m) = compileMethods(compiler)(code, allowMessage = allowMessage) + convertMethod(m) + } + + def assertSameCode(method: Method, expected: List[Instruction]): Unit = assertSameCode(method.instructions.dropNonOp, expected) + def assertSameCode(actual: List[Instruction], expected: List[Instruction]): Unit = { + assert(actual === expected, s"\nExpected: $expected\nActual : $actual") + } + + def assertSameSummary(method: Method, expected: List[Any]): Unit = assertSameSummary(method.instructions, expected) + def assertSameSummary(actual: List[Instruction], expected: List[Any]): Unit = { + def expectedString = expected.map({ + case s: String => s""""$s"""" + case i: Int => opcodeToString(i, i) + }).mkString("List(", ", ", ")") + assert(actual.summary == expected, s"\nFound : ${actual.summaryText}\nExpected: $expectedString") + } + + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } + + def assertDoesNotInvoke(m: Method, method: String): Unit = assertDoesNotInvoke(m.instructions, method) + def assertDoesNotInvoke(l: List[Instruction], method: String): Unit = { + assert(!l.exists { + case i: Invoke => i.name == method + case _ => false + }, l.stringLines) + } + + def assertInvokedMethods(m: Method, expected: List[String]): Unit = assertInvokedMethods(m.instructions, expected) + def assertInvokedMethods(l: List[Instruction], expected: List[String]): Unit = { + def quote(l: List[String]) = l.map(s => s""""$s"""").mkString("List(", ", ", ")") + val actual = l collect { case i: Invoke => i.owner + "." + i.name } + assert(actual == expected, s"\nFound : ${quote(actual)}\nExpected: ${quote(expected)}") + } + + def assertNoIndy(m: Method): Unit = assertNoIndy(m.instructions) + def assertNoIndy(l: List[Instruction]) = { + val indy = l collect { case i: InvokeDynamic => i } + assert(indy.isEmpty, indy) + } + + def getSingleMethod(classNode: ClassNode, name: String): Method = + convertMethod(classNode.methods.asScala.toList.find(_.name == name).get) + + def findAsmMethods(c: ClassNode, p: String => Boolean) = c.methods.iterator.asScala.filter(m => p(m.name)).toList.sortBy(_.name) + def findAsmMethod(c: ClassNode, name: String) = findAsmMethods(c, _ == name).head + + /** + * Instructions that match `query` when textified. + * If `query` starts with a `+`, the next instruction is returned. + */ + def findInstr(method: MethodNode, query: String): List[AbstractInsnNode] = { + val useNext = query(0) == '+' + val instrPart = if (useNext) query.drop(1) else query + val insns = method.instructions.iterator.asScala.filter(i => textify(i) contains instrPart).toList + if (useNext) insns.map(_.getNext) else insns + } + + def assertHandlerLabelPostions(h: ExceptionHandler, instructions: List[Instruction], startIndex: Int, endIndex: Int, handlerIndex: Int): Unit = { + val insVec = instructions.toVector + assertTrue(h.start == insVec(startIndex) && h.end == insVec(endIndex) && h.handler == insVec(handlerIndex)) + } + + import scala.language.implicitConversions + + implicit def aliveInstruction(ins: Instruction): (Instruction, Boolean) = (ins, true) + + implicit class MortalInstruction(val ins: Instruction) extends AnyVal { + def dead: (Instruction, Boolean) = (ins, false) + } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } +} -- cgit v1.2.3