blob: 12ecffe682f06d7fcaa575c31eb3c20e22558776 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.metalava.cli.common
import com.android.tools.metalava.OptionsDelegate
import com.android.tools.metalava.ProgressTracker
import com.android.tools.metalava.run
import com.android.tools.metalava.testing.TemporaryFolderOwner
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.rules.ErrorCollector
import org.junit.rules.TemporaryFolder
/**
* Base class for command related tests.
*
* Tests that need to run command tests must extend this and call [commandTest] to configure the
* test.
*/
abstract class BaseCommandTest<C : CliktCommand>(internal val commandFactory: () -> C) :
TemporaryFolderOwner {
/**
* Collects errors during the running of the test and reports them at the end.
*
* That allows a test to report multiple failures rather than just stopping at the first
* failure. This should be used sparingly. In particular, it must not be used to create test
* methods that perform multiple distinct tests. Those should be split apart into separate
* tests.
*/
@get:Rule val errorCollector = ErrorCollector()
/** Provides access to temporary files. */
@get:Rule override val temporaryFolder = TemporaryFolder()
@Before
fun ensureTestDoesNotAccessOptionsLeakedFromAnotherTest() {
OptionsDelegate.disallowAccess()
}
@After
fun ensureTestDoesNotLeakOptionsToAnotherTest() {
OptionsDelegate.disallowAccess()
}
/**
* Type safe builder for configuring and running a command related test.
*
* This creates an instance of [CommandTestConfig], passes it to lambda expression for
* modification and then calls [CommandTestConfig.runTest].
*/
fun commandTest(init: CommandTestConfig<C>.() -> Unit) {
val config = CommandTestConfig(this)
config.init()
config.runTest()
}
}
/**
* Contains configuration for a test that uses `Driver.`[run]
*
* It is expected that the basic capabilities provided by this class will be extended to add the
* capabilities needed by each test. e.g.
* * Tests for a specific sub-command could add extension functions to specify the different options
* and arguments.
* * Extension functions could also be added for groups of options that are common to a number of
* different sub-commands.
*/
class CommandTestConfig<C : CliktCommand>(private val test: BaseCommandTest<C>) {
/**
* The args that will be passed to `Driver.`[run].
*
* This is a val rather than a var to force any builder extension to append to them rather than
* replace then. That should result in builder extensions that can be more easily combined into
* a single test.
*/
val args = mutableListOf<String>()
/**
* The expected output, defaults to an empty string.
*
* This will be checked after running the test.
*/
var expectedStdout: String = ""
/**
* The expected output, defaults to an empty string.
*
* This will be checked after running the test.
*/
var expectedStderr: String = ""
/**
* The command that is being tested.
*
* This must only be accessed in a [verify] block.
*/
lateinit var command: C
/** The list of lambdas that are invoked after the command has been run. */
val verifiers = mutableListOf<() -> Unit>()
/** Create a temporary folder. */
fun folder(): File = test.temporaryFolder.newFolder()
/**
* Create a file that can be passed as an input to a command.
*
* @param name the name of the file, relative to parentDir.
* @param contents the contents of the file.
* @param parentDir the optional parent directory within which the file will be created. If it
* is not provided then the file will just be created in a test specific temporary folder.
*/
fun inputFile(name: String, contents: String, parentDir: File? = null): File {
val f = parentDir?.resolve(name) ?: test.temporaryFolder.newFile(name)
f.parentFile.mkdirs()
f.writeText(contents)
return f
}
/**
* Get the path to a file that can be passed as an output from a command.
*
* @param name the name of the file, relative to parentDir.
* @param parentDir the optional parent directory within which the output file will be created.
* If it is not provided then the file will just be created in a test specific temporary
* folder.
*/
fun outputFile(name: String, parentDir: File? = null): File {
val f = parentDir?.resolve(name) ?: test.temporaryFolder.newFile(name)
f.parentFile.mkdirs()
return f
}
/**
* Add a lambda function verifier that will check some result of the test to the list of
* verifiers that will be invoked after the command has been run.
*
* All failures reported by the verifiers are collated and reported at the end so each verifier
* must be standalone and not rely on the result of a preceding verifier.
*
* @param position the optional position in the list, by default they are added at the end.
* @param verifier the lambda function that performs the check.
*/
fun verify(position: Int = verifiers.size, verifier: () -> Unit) {
verifiers.add(position, verifier)
}
/**
* Wrap an assertion to convert it to a non-fatal check that is reported at the end of the test.
*
* e.g. the following will report all the assertion failures at the end of the test.
*
* check {
* assertEquals("foo", "bar")
* }
* check {
* assertEquals("bill", "ted")h
* }
*
* This should be used sparingly. In particular, it must not be used to create test methods that
* perform multiple distinct tests. Those should be split apart into separate tests.
*/
fun check(body: () -> Unit) {
try {
body()
} catch (e: Throwable) {
if (e is AssertionError || e is Exception) {
test.errorCollector.addError(e)
} else {
throw e
}
}
}
/** Run the test defined by the configuration. */
internal fun runTest() {
val stdout = StringWriter()
val stderr = StringWriter()
val printOut = PrintWriter(stdout)
val printErr = PrintWriter(stderr)
// Runs the command
command = test.commandFactory()
runCommand(printOut, printErr, command)
// Add checks of the expected stderr and stdout at the head of the list of verifiers.
verify(0) { Assert.assertEquals(expectedStderr, test.cleanupString(stderr.toString())) }
verify(1) { Assert.assertEquals(expectedStdout, test.cleanupString(stdout.toString())) }
// Invoke all the verifiers.
for (verifier in verifiers) {
// A failing verifier will not break the
check { verifier() }
}
}
private fun runCommand(printOut: PrintWriter, printErr: PrintWriter, command: C) {
val progressTracker = ProgressTracker(stdout = printOut)
val metalavaCommand =
MetalavaCommand(
stdout = printOut,
stderr = printErr,
defaultCommandFactory = { FakeDefaultCommand() },
progressTracker,
)
metalavaCommand.subcommands(command)
metalavaCommand.process(args.toTypedArray())
}
private class FakeDefaultCommand : CliktCommand() {
override fun run() {
throw NotImplementedError("Should never be called")
}
}
}