INRIA / spoon

Spoon is a metaprogramming library to analyze and transform Java source code. :spoon: is made with :heart:, :beers: and :sparkles:. It parses source files to build a well-designed AST with powerful analysis and transformation API.
http://spoon.gforge.inria.fr/
Other
1.74k stars 347 forks source link

add support for reusable architectural tests #3638

Open monperrus opened 3 years ago

monperrus commented 3 years ago

Spoon is great for writing architectural tests, see http://spoon.gforge.inria.fr/architecture_test.html

Some tests we write could actually be useful for lots of Java projects, such as the recent one that checks for unused fields.

We could package the reusable architectural tests in some way so that we can share them with the world.

List of already implemented reusable architectural tests:

monperrus commented 3 years ago

Architectural checks is the most exciting topic, because the other 3 are "solved" to some degree. Do you have some ideas for checks spoons is missing?

All tests that I thought of are already implemented :-) For identifying new ones, I would recommend to ask practitioners. I can introduce you to relevant persons if you'd like to proceed this way.

MartinWitt commented 3 years ago

It would be a pleasure to proceed this way and gain more knowledge in this field.

MartinWitt commented 3 years ago

After reading @vmassol mail, I would describe the current state as following: Current state:

So there is some need for a better solution.

Xwiki Checks

After reading the the xwiki tests, I tried to formulate them as architectural rule. The rules follow a common pattern. Lets first look at the xwiki architectural checks:

Requirement/Rule:

Verify that @Unstable annotation don't stay too long (and that a @since annotation is present too)

=> Each element that has a @Unstable Annotation also has a @since annotation. => Each @since annotation isn't too old.

Requirement/Rule:

Verify that Script Services are not located in the internal package.

=> Each class(?) that is script service, doesn't have a package name containing internal.

Requirement/Rule:

Verify that @since javadoc tags have the correct format (and that they don't contain several versions per tag).

=> Each @since annotation has only 1 value, and the value is conform to format.

Requirement/Rule:

Verify none of the listed methods are called in our Java code

=> For every method invocation, the method name is never .../ is not part of a list.

Requirement/Rule:

Verify that JUnit4 and JUnit5 APIs are not both used at the same time in a test

=> For every method, holds there is no method/field from junit 4 AND junit 5.

Requirement/Rule:

Verify that JUnit tests don't output content to stdout or stderr.

=> For every method, holds there is no reference to system.out/system.err

Generalization

We see a base rule:

Base rule:

∀ astElement FilterRule(a) => condition(a)

FilterRule

Element instanceof $WantedType AND ....

e.g. Every private field...

Condition:

e.g. is called

The filter rule is a selector for elements. Elements could be filtered by

After this short analysis I propose the following structure.

Framework structure

Every architecture check should consist of 2 methods. A filter and condition method, comparable to a junit test case. For filter and condition the framework provides out of the box implementations and allows defining own implementations. Here is the goal to provide an easy API for common checks but allow defining more complex checks.

       |Spoon|
          ↑
  |Architectural Checks|
          ↑
  |XWiki-Architectural Checks|

XWiki Test cases

Shall we write a framework for xwiki only? I would say no, but we should use xwiki as a partner to gather the user needs. A good result would be that a xwiki developer could easily define new check without knowledge about java-asts and every check is readable for a normal user.

Junit Dependency

The architectural checks shouldn't be junit cases by default, but allow them. If we simply throw exceptions a user can wrap them in a junit case or provide use the provided runner. I personally hate mixing unit tests and architectural test cases. Both have different concerns and should fail independently. Unit tests show verifies the code, architectural checks keep the code quality up.

Spoon integration

We should not deliver the framework with spoon-core, but as a module. This keeps spoon modular and is done for cases like spoon-maven-plugin. I'm unsure where to place the code because there are some possibilities

Next steps

Wdyt about this plan? Any comments?

monperrus commented 3 years ago

Thanks for the wrap-up.

Shall we write a framework for xwiki only?

No, we extract from xwiki what's reusable in other projects.

I'm unsure where to place the code because there are some possibilities

A folder in spoon is perfect, as the other modules

vmassol commented 3 years ago

Nice summary. Cool!

Note that we have plenty of other small checks in the maven enforcer plugins (you can find all the skip properties there: https://github.com/xwiki/xwiki-commons/blob/master/pom.xml#L121). For example: https://github.com/xwiki/xwiki-commons/blob/master/pom.xml#L1373

MartinWitt commented 3 years ago

As my current implementation is still heavy wip, here a small update. Currently checks following the style at the bottom look like the way to go.

monperrus commented 3 years ago

Cool. What's the intention of @Architecture? Which driver acts upon it?

MartinWitt commented 3 years ago

We have a CheckRunner with a method to invoke all methods with this annotation. A comparable annotation is @Test. This allows to run all architecture tests with on method. It's the entry point of the api for a launch. A cli like Spring-Shell, a maven plugin and junit-test case can reuse this entry point.
The intention is to provide one entry point for running all architecture tests without any external component/dependency for invocation.

    public void runChecks(String srcPath, String testPath) {
        ModelBuilder builder = new ModelBuilder(srcPath, testPath);
        builder.getTestModel().getElements(new TypeFilter<CtMethod<?>>(CtMethod.class)).stream()
                .filter(v -> v.hasAnnotation(Architecture.class))
                .map(v -> v.getReference().getActualMethod())
                .filter(Objects::nonNull)
                .forEach(clazz -> invokeMethod(builder, clazz));
    }
monperrus commented 3 years ago

A cli like Spring-Shell, a maven plugin and junit-test case can reuse this entry point.

We have to think of the user experience. What would be the simplest way to activate the Spoon architectural checks in a project?

MartinWitt commented 3 years ago

Currently the simplest way is wrap the runner in a junit test. This enables integration like run, debug in most IDEs. The CheckerRunner currently looks in the test path for all methods annotated with @Architecture and invokes them. A starter solution looks like the following, we could even set both paths as default.

    @Test
    public void allChecksMustRun() {
        CheckerRunner runner = new CheckerRunner();
        runner.runChecks("./src/main/java", "./src/test/java");
    }

Edit: I try to get the current code state in a readable form today to enhance discussions

monperrus commented 3 years ago

we could even set both paths as default

yes, we want that :)

Plus some renaming:

    @Test
    public void allChecksMustRun() {
        new SpoonArchitecturalChecker().check();
    }
monperrus commented 3 years ago

FYI, about architectural tests in CI:

Architecture violations detection and visualization in the continuous integration pipeline (SPE 2021) https://onlinelibrary.wiley.com/doi/10.1002/spe.3004?af=R