Giter Site home page Giter Site logo

exemplar's Introduction

Exemplar - check and present samples for CLI tools

Given a collection of sample projects, this library allows you to verify the samples' output.

It does this by discovering, executing, then verifying output of sample projects in a separate directory or embedded within asciidoctor. In fact, the samples embedded here are verified by ReadmeTest.

Use cases

The intent of the samples-check module is to ensure that users of your command-line tool see what you expect them to see. It handles sample discovery, normalization (semantically equivalent output in different environments), and flexible output verification. It allows any command-line executable on the PATH to be invoked. You are not limited to Gradle or Java.

While this has an element of integration testing, it is not meant to replace your integration tests unless the logging output is the only result of executing your tool. One cannot verify other side effects of invoking the samples, such as files created or their contents, unless that is explicitly configured.

This library is used to verify the functionality of samples in Gradle documentation.

Installation

First things first, you can pull this library down from Gradle’s artifactory repository. This Gradle Kotlin DSL script shows one way to do just that.

Example 1. Installing with Gradle
build.gradle.kts
plugins {
    id("java-library")
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.gradle.exemplar:samples-check:1.0.0")
}
$ gradle check

Usage

Configuring external samples

An external sample consists of a directory containing all the files required to run that sample. It may include tagged content regions that can be extracted into documentation.

You can configure a sample to be tested by creating a file ending with .sample.conf (e.g. hello-world.sample.conf) in a sample project dir. This is a file in HOCON format that might look something like this:

quickstart.sample.conf
executable: bash
args: sample.sh
expected-output-file: quickstart.sample.out

or maybe a more complex, multi-step sample:

incrementalTaskRemovedOutput.sample.conf
commands: [{
  executable: gradle
  args: originalInputs incrementalReverse
  expected-output-file: originalInputs.out
  allow-additional-output: true
}, {
  executable: gradle
  args: removeOutput incrementalReverse
  flags: --quiet
  expected-output-file: incrementalTaskRemovedOutput.out
  allow-disordered-output: true
}]

When there are multiple steps specified for a sample, they are run one after the other, in the order specified. The 'executable' can be either a command on the $PATH, or gradle (to run Gradle), or cd (to change the working directory for subsequent steps).

See Sample Conf fields for a detailed description of all the possible options.

NOTE: There are a bunch of (tested) samples under samples-check/src/test/samples of this repository you can use to understand ways to configure samples.

Configuring embedded samples

An embedded sample is one in which the source for the sample is written directly within an Asciidoctor source file.

Use this syntax to allow samples-discovery to extract your sources from the doc, execute the sample-command, and verify the output matches what is declared in the doc.

.Sample title
[.testable-sample]       // (1)
====

.hello.rb                // (2)
[source,ruby]            // (3)
-----
puts "hello, #{ARGV[0]}" // (4)
-----

[.sample-command]        // (5)
-----
$ ruby hello.rb world    // (6)
hello, world             // (7)
-----

====
  1. Mark blocks containing your source files with the role testable-sample

  2. The title of each source block should be the name of the source file

  3. All source blocks with a title are extracted to a temporary directory

  4. Source code. This can be `include::`d

  5. Exemplar will execute the commands in a block with role sample-command. There can be multiple blocks.

  6. Terminal commands should start with "$ ". Everything after the "$ " is treated as a command to run. There can be multiple commands in a block.

  7. One or more lines of expected output

[NOTE] All sources have to be under the same block, and you must set the title of source blocks to a valid file name.

Verify samples

You can verify samples either through one of the JUnit Test Runners or use the API.

Verifying using a JUnit Runner

This library provides 2 JUnit runners SamplesRunner (executes via CLI) and GradleSamplesRunner (executes samples using Gradle TestKit). If you are using GradleSamplesRunner, you will need to add gradleTestKit() and SLF4J binding dependencies as well:

dependencies {
    testImplementation(gradleTestKit())
    testRuntimeOnly("org.slf4j:slf4j-simple:1.7.16")
}

NOTE: GradleSamplesRunner supports Java 8 and above and ignores tests when running on Java 7 or lower.

To use them, just create a JUnit test class in your test sources (maybe something like src/integTest/com/example/SamplesIntegrationTest.java, keeping these slow tests separate from your fast unit tests.) and annotate it with which JUnit runner implementation you’d like and where to find samples. Like this:

SamplesRunnerIntegrationTest.java
package com.example;

import org.junit.runner.RunWith;
import org.gradle.exemplar.test.runner.GradleSamplesRunner;
import org.gradle.exemplar.test.runner.SamplesRoot;

@RunWith(GradleSamplesRunner.class)
@SamplesRoot("src/docs/samples")
public class SamplesIntegrationTest {
}

When you run this test, it will search recursively under the samples root directory (src/docs/samples in this example) for any file with a *.sample.conf suffix. Any directory found to have one of these will be treated as a sample project dir (nesting sample projects is allowed). The test runner will copy each sample project to a temporary location, invoke the configured commands, and capture and verify logging output.

Verifying using the API

Use of the JUnit runners is preferred, as discovery, output normalization, and reporting are handled for you. If you want to write custom samples verification or you’re using a different test framework, by all means go ahead :) — please contribute back runners or normalizers you find useful!

You can get some inspiration for API use from SamplesRunner and GradleSamplesRunner.

Command execution is handled in the org.gradle.exemplar.executor.* classes, some output normalizers are provided in the org.gradle.exemplar.test.normalizer package, and output verification is handled by classes in the org.gradle.exemplar.test.verifier package.

Using Samples for other integration tests

You might want to verify more than just log output, so this library includes JUnit rules that allow you to easily copy sample projects to a temporarily location for other verification. Here is an example of a test that demonstrates use of the @Sample and @UsesSample rules.

BasicSampleTest.java
package com.example;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.gradle.exemplar.test.rule.Sample;
import org.gradle.exemplar.test.rule.UsesSample;

public class BasicSampleTest {
    public TemporaryFolder temporaryFolder = new TemporaryFolder()
    public Sample sample = Sample.from("src/test/samples/gradle")
            .into(temporaryFolder)
            .withDefaultSample("basic-sample")

    @Rule
    public TestRule ruleChain = RuleChain.outerRule(temporaryFolder).around(sample)

    @Test
    void verifyDefaultSample() {
        assert sample.getDir() == new File(temporaryFolder.getRoot(), "samples/basic-sample");
        assert sample.getDir().isDirectory();
        assert new File(sample.getDir(), "build.gradle").isFile();

        // TODO(You): Execute what you wish in the sample project
        // TODO(You): Verify file contents or whatever you want
    }

    @Test
    @UsesSample("composite-sample/basic")
    void verifyOtherSample() {
        // TODO(You): Utilize sample project under samples/composite-sample/basic
    }
}

External sample.conf reference

One of executable or commands are required at the root. If executable is found, the sample will be considered a single-command sample. Otherwise, commands is expected to be an Array of Commands:

  • repeated Command commands — An array of commands to run, in order.

A Command is specified with these fields.

  • required string executable — Executable to invoke.

  • optional string execution-subdirectory — Working directory in which to invoke the executable. If not specified, the API assumes ./ (the directory the sample config file is in).

  • optional string args — Arguments for executable. Default is "".

  • optional string flags — CLI flags (separated for tools that require these be provided in a certain order). Default is "".

  • optional string expected-output-file — Relative path from sample config file to a readable file to compare actual output to. Default is null. If not specified, output verification is not performed.

  • optional boolean expect-failure — Invoking this command is expected to produce a non-zero exit code. Default: false.

  • optional boolean allow-additional-output — Allow extra lines in actual output. Default: false.

  • optional boolean allow-disordered-output — Allow output lines to be in any sequence. Default: false.

Output normalization

samples-check allows actual output to be normalized in cases where output is semantically equivalent. You can use normalizers by annotating your JUnit test class with @SamplesOutputNormalizers and specifying which normalizers (in order) you’d like to use.

@SamplesOutputNormalizers({JavaObjectSerializationOutputNormalizer.class, FileSeparatorOutputNormalizer.class, GradleOutputNormalizer.class})

Custom normalizers must implement the OutputNormalizer interface. The two above are included in check.

Common sample modification

samples-check supports modifying all samples before they are executed by implementing the SampleModifier interface and declaring SampleModifiers. This allows you to do things like set environment properties, change the executable or arguments, and even conditionally change verification based on some logic. For example, you might prepend a Command that sets up some environment before other commands are run or change expect-failure to true if you know verification conditionally won’t work on Windows.

@SampleModifiers({SetupEnvironmentSampleModifier.class, ExtraCommandArgumentsSampleModifier.class})

Custom Gradle installation

To allow Gradle itself to run using test versions of Gradle, the GradleSamplesRunner allows a custom installation to be injected using the system property "integTest.gradleHomeDir".

Contributing

Build status
code of conduct

Releasing

  1. Change the version number in the root build script to the version you want to release.

  2. Add missing changes to the Changes section below.

  3. Push the changes to main branch.

  4. Run the Verify Exemplar job on the commit you want to release.

  5. Run the Publish Exemplar job on the commit you want to release. This job publishes everything to the Maven Central staging repository.

  6. Login to Sonatype, close the staging repository after reviewing its contents.

  7. Release the staging repository.

  8. Tag the commit you just release with the version number git tag -s VERSION -m "Tag VERSION release" && git push --tags

  9. Go to the Releases section on GitHub and create a new release using the tag you just pushed. Copy the release notes from the Changes section into the release description.

  10. Bump the version in the root build script to the next snapshot version. Push the change to main branch.

Changes

1.0.2

  • Published with new PGP key (no functional changes since 1.0.0)

1.0.1

  • Published with new PGP key (no functional changes since 1.0.0)

1.0.0

  • Publish all artifacts to the Maven Central repository under org.gradle.exemplar group

  • Renamed modules from sample-check and sample-discovery to samples-check and samples-discovery

  • Changed root package name from org.gradle.samples to org.gradle.exemplar

0.12.6

  • AsciidoctorCommandsDiscovery now support user-inputs for providing inputs to a command

  • Introduce a output normalizer removing leading new lines

0.12.5

  • GradleOutputNormalizer can normalize incubating feature message

0.12.4

  • Ensure output normalizer aren’t removing trailing new lines (except for the TrailingNewLineOutputNormalizer

0.12.3

  • GradleOutputNormalizer can normalize snapshot documentation urls

0.12.2

  • GradleOutputNormalizer can normalize wrapper download message

  • TrailingNewLineOutputNormalizer can normalize empty output

0.12.1

  • GradleOutputNormalizer can normalize build scan urls

0.12

  • Rename AsciidoctorAnnotationNormalizer to AsciidoctorAnnotationOutputNormalizer

  • Introduce WorkingDirectoryOutputNormalizer to normalize paths in the output

0.11.1

  • Introduce AsciidoctorCommandsDiscovery to only discover commands

0.11

  • Downgraded AsciidoctorJ to 1.5.8.1 to play nice with Asciidoctor extension used by Gradle documentation.

Note: The upgrade of AsciidoctorJ will need to be cross-cutting.

0.10

  • Fixes to the AsciidoctorSamplesDiscovery classes

  • Allow configuring the underlying Asciidoctor instance used by AsciidoctorSamplesDiscovery

  • A bunch of out-of-the-box OutputNormalizer

0.8

  • Handle cd <dir> commands, to keep track of the user’s working directory and apply it to later commands in the same sample.

exemplar's People

Contributors

adammurdoch avatar big-guy avatar britter avatar eriwen avatar ghale avatar jlleitschuh avatar lacasseio avatar ljacomet avatar lptr avatar marcphilipp avatar pbielicki avatar pioterj avatar rieske avatar stefma avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

exemplar's Issues

Allow configuring the target name for a sample directory

After #6 is merged, there is still one use-case missing for replacing the Sample Rule in gradle/gradle: The Sample rule allows configuring testSampleDirName, the target directory name for the sample.

I tried removing that functionality in the gradle/gradle code base, but I ran into long path issues on Windows: gradle/gradle#5713.

So there should be a possibility to configure the target directory where a sample is copied to, either by specifying a fixed relative path to the target base dir or a target directory directly.

I think using a target directory supplied as a File would suffice. This field could then be wired in the method computeSampleDir.

The configuration file should provide a way to define a human-readable test name

Expected Behavior

The sample.conf defines a key for defining a test name. For example: description: "plugin should create the task named x". Upon test execution, the test name uses the description instead of auto-generated description.

Current Behavior

The test name is derived of the directory hierarchy and the prefix of the sample.conf file. The test name is hard to read and doesn't really express its intent.

Context

Auto-discovering tests with one of the Runner implementations produces cryptic test names.

Publish to Maven Central

Context

We should resolve this library from Maven Central so consumers do not have to access repo.gradle.org. This will be faster and more stable.

@marcphilipp has recommended that we use the group org.gradle.exemplar. I agree.

Publish library to JCenter

At the moment you have to define an additional repository just for this dependency.

repositories {
    jcenter()
    maven {
        url = uri("https://repo.gradle.org/gradle/libs")
    }
}

The dependency should be available in JCenter.

Sample-check brings in slf4j-simple dependency

https://github.com/gradle/exemplar/blob/master/sample-check/build.gradle.kts#L15-L17

The problem with this is that it collides with Gradle's own SLF4J implementation, producing the following output whenever an integration test is executed:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [file:/Users/lptr/Workspace/gradle/gradle/out/production/logging/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/lptr/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-simple/1.7.16/f0cacc3d21e1027c82006a7a6f6cf2cad86d2a4f/slf4j-simple-1.7.16.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext]

I guess it should be up to the user of this library to decide what SLF4J bindings they want to use.

Provide an API for listing samples

It would be useful for other tools to be able to report discovered samples and their metadata (URL, title, etc)

Expected Behavior

sample-discovery should provide an API that lists samples and their metadata.

Current Behavior

No such API exists

Context

A couple Gradle projects have requested the ability to have indexed samples. Listing samples in a report is the first step toward this.

Discover and verify samples embedded in Markdown

remark seems like it may be capable of extracting what we need from Markdown documents and executing samples.

Given that markdown is more widely used than Asciidoctor, this may greatly increase the possible user base of this library.

Preserve sample directory name when running checks

Currently the temp directory used when running sample-check has a different name. This cases Gradle project names to differ when one is not explicitly set in settings.gradle. This should be preserved.

Prevent duplication of gradle command between docs and sample config

There is no longer a way for the user manual to get the command (e.g. gradle help --quiet) to be run for each sample, and it will be duplicated.

Possible approaches:

  • Use an Asciidoctor extension that reads a given sample config file and extracts the command
  • Expose the command through a yet-unwritten sample-expo library that reports on available samples

Relevant docs:

Allow normalization on both sides

Expected Behavior

It should be possible to have expected output normalization, just like we have actual output normalization.

Current Behavior

Currently, only the actual output is normalized.

Context

In dependency management, we often have quite large outputs of dependency graphs, and often from one version to another, we have insignificant chances like number of spaces, due to the fact that this attribute has been renamed or whatever. So often tests are failing because this number of spaces in the expected output has changed (or additional blank lines, or whatever). It would be nice if we could just apply normalization on both sides, so that when we "search and replace" in a sample, we don't have to bother aligning text ourselves.

The idea would be that if norm(output)==norm(expected output), then we're good, and not only if norm(output)==expected manually normalized output.

JUnit runners don't handle @Before nor @After

Using @RunWith(GradleSamplesRunner::class) or @RunWith(SamplesRunner::class)

Expected Behavior

@Before and @After functions to be invoked properly

Current Behavior

@Before and @After functions silently ignored

Context

I wanted to skip some samples under certain conditions in a @Before function using JUnit Assume. I didn't find a satisfying way so far.

@BeforeClass and @AfterClass are already handled by the parent ParentRunner.

Allow specifying custom Gradle version/distro/install

For testing purposes it would be useful to be able to use a custom Gradle version, distribution or installation.

The Gradle version or path to distro/install could be set as a test annotation or in the HOCON configuration file leveraging environment variable substitution or including a generated HOCON file.

Setting the temporary folder for Sample doesn't work

Expected Behavior

A temporary folder can be provided to Sample as root directory.

Current Behavior

The temporary isn't initialized yet when the Rule for the Sample kicks in. You will receive the following exception:

java.lang.IllegalStateException: the temporary folder has not yet been created
	at org.junit.rules.TemporaryFolder.getRoot(TemporaryFolder.java:145)
	at org.junit.rules.TemporaryFolder.newFolder(TemporaryFolder.java:94)
	at org.junit.rules.TemporaryFolder.newFolder(TemporaryFolder.java:86)
	at org.gradle.samples.test.rule.Sample$2.getDir(Sample.java:74)
	at org.gradle.samples.test.rule.Sample.computeSampleDir(Sample.java:165)
	at org.gradle.samples.test.rule.Sample.getDir(Sample.java:158)
	at org.gradle.samples.test.rule.Sample$3.evaluate(Sample.java:138)
	at org.spockframework.runtime.extension.builtin.TestRuleInterceptor.intercept(TestRuleInterceptor.java:39)
	at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:97)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:66)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:118)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:175)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:157)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.lang.Thread.run(Thread.java:748)

Context

Setting a temporary folder with any of the into methods.

@Rule
TemporaryFolder temporaryFolder = new TemporaryFolder()

@Rule
Sample sample = Sample.from("src/docs/samples").into(temporaryFolder)

Steps to Reproduce (for bugs)

See example above.

Version 0.8.0 broken when using multi-step config with `execution-subdirectory`

Expected Behavior

No broken.

Current Behavior

The worker directory keeps getting appended throughout each steps: https://github.com/gradle/exemplar/blob/master/sample-check/src/main/java/org/gradle/samples/test/runner/SamplesRunner.java#L155-L157

Context

Steps to Reproduce (for bugs)

Execute pretty much any gradle/gradle sample with version 0.8.0 and you will get an exception of cannot find directory like .../groovy/kotlin.

Your Environment

  • Build scan URL:

Make sample check configurable to use repository mirrors

When testing samples, there is no mirroring of repositories possible AFAICT. This is causing noise in CI due to the Bintray availability issues.
sample-check should be enhanced so that it can run with an option similar to GradleExecuter.withRepositoryMirrors() to reduce flakiness in execution.

Allow IDE autocomplete for Exemplar config files

Expected Behavior

As a user, I want to more easily author my sample.conf files by leveraging autocomplete

Current Behavior

No completion available

Context

It's not obvious what properties and values are available. Better to use something like JSON Schema Store or a similar mechanism to allow completion through IDEs. Though there are not very many Exemplar users yet, all of them are likely to want this feature.

Feature: Gradle Plugin samples runner

As a Gradle plugin author, I'd like to inject the Gradle plugin under test into the samples test.

Expected Behavior

Provide a GradlePluginSamplesRunner that configures withPluginClasspath() on TestKit.

Current Behavior

Context

There's now 2 plugins that want this. Be cool to provide this.

Replace 'allow-disordered-output' with a normalizer that reorders task output groups

"allow-disordered-output" is a very blunt tool to verify the output of a build that does stuff in parallel (which will more and more be the default) and was really only used because the plain console did not group output. Now that it does, a better strategy might be to use a normalizer that reorders task output groups but does not reorder the text within a group.

For example, reorder the groups alphabetically:

Normalize:

> Task :b
some output

> Task :a
other output

To:

> Task :a
other output

> Task :b:
some output

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.