These notes are meant for developers working on EvoMaster, and for people making a pull request. There are several rules of thumb regarding how to write "good code", but often rules are either too generic and not tailored for a given particular piece of software (e.g., different kinds of architectures).
The rules of thumb described here in this document are not meant to be either exhaustive nor absolute. Rigid rules are not substitute for common sense, as they are rather guidelines that can be ignored in some special cases. Furthermore, the guidelines need to be realistic and easy to use: there would be no point to ask for detailed comments on each single method/field and 100% coverage test suites...
These notes also include some explanations and motivations for some of the architectural choices made in the development of EvoMaster.
The core process of EvoMaster is built in Kotlin, as we strongly prefer it over Java. However, the client libraries for JDK SUTs (e.g., not just Java, but also all other languages that do compile to JDK bytecode) are written in Java instead of Kotlin. The main reason is that, being libraries, we do not want to also have to ship the Kotlin runtime libraries with them.
Not going to start an holy war here... once made a choice, we just keep it consistent throughout the whole project. Regarding opening braces {, the ancient texts state that those shall be on the same line of the code, and not in their own line. Therefore, the following is right(eous):
foo{
}
whereas the following blasphemy must be expunged:
foo
{
}
Note that, at the current moment, we do not use an automated-formatter as part of the build (for several reasons...).
Regarding spaces vs. tabs, I have no idea what is in use. This should be automatically handled by your IDE (and if it doesn't, switch to a better IDE).
EvoMaster uses a logging framework.
For debugging and logging errors in a class Foo
, create a logger in the following way.
- for Java:
private static Logger log = LoggerFactory.getLogger(Foo.class);
- for Kotlin:
companion object { private val log: Logger = LoggerFactory.getLogger(Foo::class.java)}
It is important to keep the same name log
to make things consistent among different classes.
If the logging should be part the actual output for the console user, then rather use:
LoggingUtil.getInfoLogger()
Writing something like:
log.debug("this is not "+ foo + " very " + bar +" efficient")
is not efficient, as most of the time debug logs are deactivated, and concatenating strings is
expensive. Recall String
is immutable, and each +
does create a new String
object.
The above logging can be rewritten into:
log.debug("this is not {} very {} efficient", foo, bar)
Note: not a big deal for warn/error, as those are/should be rare... but it can become quite an overhead for trace/debug/info.
Better to throw an exception, as the entry point of EvoMaster does some logging when ends.
Furthermore, System.exit
becomes problematic when unit testing EvoMaster.
Static variables should be either constant or representing transient data (e.g., cache information
whose presence/missing has only effect on performance, not on functionality).
Having "classes with static state" is usually a poor OO design (an exception to this rule
is ExecutionTracer
).
If those are really needed, then you should rather use an injectable singleton service (see next point).
This is not just to be pedantic, but, really, non-constant static variables make unit testing
far much harder and lead to code that is more difficult to understand and maintain.
To avoid issues with mutable static variables, we use a dependency injection framework.
In particular, we use Guice
, extended with Governator
to handle post-construct events.
All injectable services should be singletons, and declared under a package called *.service
(this
is to make it easy to find out which services are available).
There is no auto-discovery of beans. This is done manually. The reason is that, depending on configurations, we can have many different context initializations. For example, the beans used for testing REST APIs would not be needed when testing GraphQL ones.
Unit tests should be put in the src/test/java
and src/test/kotlin
folders,
following the same package structure as EvoMaster code.
A unit test suite for SUT org.evomaster.somepackage.Foo
MUST be called org.evomaster.somepackage.FooTest
.
This is important for several reasons:
- Need to know what class the test case is supposed to unit test by just looking at its name
- Should be easy to identify if a class has a test suite for it
- If in same package, then the test suite can access package/protected fields/methods
- Having
Test
as postfix (instead of a prefix) is useful for when searching for classes by name - A
Test
postfix is a requirement for Maven to execute the test suite during the build
Besides unit tests, it is essential to have E2E ones as well.
Those should be added under the e2e-tests
module.
Being non-deterministic, we cannot guarantee that EvoMaster can always find a valid solution (e.g.,
create test cases with certain properties).
Furthermore, we cannot run the E2E tests for long time (otherwise the CI builds will take forever).
The idea is to create artificial SUTs that should be trivial to solve when some settings (which we want
to test) are on, and very difficult (if not straight-out infeasible) otherwise.
Note: current version of JUnit 5 is worse than JUnit 4 when dealing with E2E tests.
E.g., there is no handling of flaky tests (in JUnit 4, this was handled by the Surefire/Failsafe plugins).
This is the reason why such test executions should be wrapped inside a handleFlaky
call.
Also notice that, for JavaScript and C#, E2E tests are different, as run through bash scripts. This is due to the fact that we have to run 2 separate processes using different technologies (e.g., JVM vs. .Net and NodeJS).
Too long methods (e.g., more than 100 lines) should be split, as difficult to understand. For this task, in IntelliJ, you can right-click on a code snippet and choose "Refactor -> Extract -> Function". Likewise, should avoid classes with more than 1000 lines.
In the ideal world, each class/method/field would have nice, detailed, appropriate code comments. But even in such a beautiful world, everything would go to hell at the first code change, as that might require manually changing most of the code comments.
Cannot really quantify how many comments one should write, but at least it would be good to have:
- brief (1-2 sentences) description of what the class is useful for (just before the class declaration)
- for fields that are data structures (e.g., collections and arrays) some comments would be useful, as long and detailed variable names are not practical
- for
Map
s, should add a comment stating what is the key, and what is the value.
When writing a comment for a class/method/field, use JavaDoc style:
/** */
In this way, your IDE can show the comments when you hover with the mouse over them.
For C#, besides /** */
, for single line documentation you can use a triple slash ///
.
If a call on a object has side effects outside the class itself (e.g., writing to disk, add a system hook thread), then this needs to be documented (see point on how to write comments), unless it is obvious from the function/class name.
- Pre-conditions of
public
methods should throw exceptions explicitly (e.g.,IllegalArgumentException
andIllegalStateException
). Whenever possible, it is worth to write pre-conditions topublic
methods. - Pre-conditions of
private
methods and post-conditions (bothpublic
andprivate
methods) should use the keywordassert
in Java, and the functionassert()
in Kotlin. (An exception is when the validation of inputs of a public method is delegated/moved to aprivate
method: in this case you could addthrow
.) Post-conditions are good, but often are difficult to write. Note: a post-condition does not to be complete to be useful (i.e., find bugs). For example, if we have A && B, but the writing of B is too difficult (or time-consuming), still having just A as post-condition can help
Note: currently Kotlin does not have lazily evaluated assertions.
If you are writing a computational expensive check, rather user Lazy.assert(predicate)
.
When writing a new class (or re-factoring a current one), fields should come first, followed by class constructors and then the other methods.
EvoMaster uses randomized algorithms. Running it twice on the same application can give
different results.
This is a problem for testing and debugging EvoMaster itself, as for example the test cases
will be flaky.
To avoid such issues, we must control the source of non-determinism.
All randomness sources MUST come from the Randomness
class.
Some data-structures could lead to non-deterministic behavior (e.g., iteration over a Set
does not
guarantee the order).
This does not seem the case for the default data-structures in Kotlin, but it is definitively
a problem in Java, e.g., HashSet
vs. LinkedHashSet
.
In EvoMaster we do have checks for its determinism. This is achieved by running some E2E tests twice
with verbose logging, and then compare the logs for an exact match.
If some logs are not deterministic (e.g., printing out for how many seconds the search ran), those should
be inside a check for EMConfig.avoidNonDeterministicLogs
.
When running EvoMaster on an application, the seed for the random generator is taken from
the CPU clock.
To make a run deterministic, you will need to use the --seed
option to specify a constant seed.
We follow the typical naming convention used in Java
: class names start in capital letter
(e.g., class Foo
), whereas we use camel-case for variables and
methods (e.g., void fooBar()
and String helloWorld;
).
Constants in Java
(but usually not in Kotlin
, unless they are global public variables in a companion object)
would be typically in upper-case using snake-case
(e.g., final String HELLO_WORLD
).
Kebab-case should be avoided for names of classes/methods/variables
(e.g., no String hello-world
, which anyway would not compile).
Regarding packages and modules, it is a bit more tricky. In this project, the current
rules are the following (but might change if given arguments for a better approach):
no dashes -
and no upper-case in the package names, but -
are fine (and preferable) in module names.
For example, org.EvoMaster.foo-bar
would be wrong for 2 reasons, which could be fixed
with org.evomaster.foobar
or org.evomaster.foo.bar
.
On the other hand, a Maven module called foo-bar
would be fine,
but not Foo-bar
.
The motivation here is that modules are mapped to folders on the operating system,
and we need to avoid issues with OSs like Windows that are case insensitive, and with .
treated as beginning of a file extension.
All code written for EvoMaster
must be inside the package org.evomaster.*
.
Each module must define a subpackage, with a name somehow related to the module itself.
Dashes -
in the module name would be either stripped or replaced with dots .
.
For example, a module called controller-api
under the module client-java
could
define a package called org.evomaster.client.java.controller.api
.
Note that it is imperative that no module defines the same subpackage, as to avoid
class name conflicts.
All names should use ASCII letters. Non-ASCII ones like ø or Å must be avoided.
EvoMaster
is built with Maven
, with a hierarchy of submodules.
Given a module X
declaring a submodule Y
with <module>
in its pom.xml
file,
then Y
must declare X
as parent with <parent>
.
Do no break the hierarchy by pointing to a parent outside EvoMaster
(e.g.,
something like spring-boot-starter-parent
).
If you need to use such external poms, you can import them as dependency, i.e., specifying
the <scope>import</scope>
tag.
DEPRECATED: When creating a new module, it is also important to add it as a dependency to report
,
so that aggregated, transitive code coverage can be calculated.
All dependency <version>
tags must be declared in the root pom.xml
file,
in the <dependencyManagement>
section.
Submodules must not declare a version for a library, and rather refer to the ones in
the root using just <groupId>
and <artifactId>
(but possibly overriding some configurations,
like <scope>
).
Motivation: must have only a single version of a library in EvoMaster
. Specifying versions
in submodules can lead to duplicated <version>
declarations with different version numbers.
All version numbers should be easily audited, and so should be in a single file (i.e., the
root pom.xml
).
There are cases in which we might need different versions of the same library in different modules (e.g., recall the difference between core
, client
and e2e
modules).
And the are cases as well in which adding a dependency management definition can have side effects on transitively imported libraries.
In those cases, a dependency management declaration in the root pom file would be problematic.
In such a case, should still have the version number declared as a property (see <properties>
entry) in the root pom file.
In general, adding a new dependency is fine, but few things to consider:
-
NEVER ever add a GPL licensed library, unless it is under the so called classpath exception. Note that LGPL libraries are fine.
-
When adding a new library, check who is maintaining it, and when was its last update. No longer maintained libraries should be avoided.
-
Libraries might need to be shaded if added to the client controller module.
-
Best to always ask the team lead before adding any new library (especially if you do not know what shading is).
As a rule of thumb, to avoid possible issues with copyrights and license compliance, we should not include code directly from third-party sources. However, when that happens, it MUST be made clear in the files themselves (e.g., with comments in their top, with URLs of the original sources). Furthermore, this information should also be added to the reused_code.md file.
If you are among the core developers of EvoMaster
, you should get an invitation to join
Trello.
We use it to track activities and assign tasks. Anyone can create new tasks/cards.
Current usage:
On going
: tasks that are currently under development. Those must be assigned to at least 1 person.Done
: tasks that are fully done. We do not delete them, e.g., just in case if need to look at them again in the future. Even when a task is completed, the moving fromOn going
toDone
should be carried out only during a developer meeting (so it can be demoed or at least discussed). Furthermore, a done task should be added on top of theDone
list. In this way, by looking at the top of the list, one can see what were the most recent changes.Important, to do soon
: high priority tasks which have not been started yet.Issues/bugs
: reported bugs which are not trivial to fix. For developers, better to report them here than GitHub issue page.Backlog-*
: different backlogs, divided by topic.
Only the project manager should make a new release, as it requires a password. Instructions can be found here.
At this point, we only support JDK 8 and the following major LTS versions.
EvoMaster must be built with JDK 8, but still must be able to run it with the most recent LTS JDK.
Can be useful to setup your machine to easily switch between different JDK versions.
For example, if you are using a Mac, in your ~/.profile
configuration, you could have something
like:
export JAVA_HOME_8=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/
export JAVA_HOME_11=/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/
export JAVA_HOME=$JAVA_HOME_11
export PATH=$JAVA_HOME/bin:$PATH
alias java8='$JAVA_HOME_8/bin/java'
alias java11='$JAVA_HOME_11/bin/java'
alias mvn8='JAVA_HOME=$JAVA_HOME_8 && mvn'
alias mvn11='JAVA_HOME=$JAVA_HOME_11 && mvn'
If you are using Windows, it does not seem there is a simple way to define aliases.
Besides setting up the JAVA_HOME
environment variable, it can be useful to set up an environment variable for each LTS JDK version, e.g., JAVA_HOME_8
, JAVA_HOME_11
and JAVA_HOME_17
(of course, you will need to install all those JDKs...).
Then, from a bash shell (e.g., Git Bash), you can build with Maven using:
JAVA_HOME=$JAVA_HOME_11 mvn <your_inputs>
For example, try it with:
JAVA_HOME=$JAVA_HOME_11 mvn --version
You can also call java
directly with:
$JAVA_HOME_17/bin/java -version