Comments (36)
I don't think that the proposed Element annotation would ever conflict with that API, except when someone tests an annotation processor that processes annotations that are used in an annotation processor (like google testing their @AutoService
annotation processor or something), but if you indeed remove the case from the annotation, you can rename it to @Label.
I do think that it would be beneficial if there still was a @CaseLabel
annotation or something and that you could use a @ParameterizedTest
with a @CaseLabelSource
to which you provide the label that was used in the @CaseLabel
annotation.
from elementary.
For the single()
implementation you can also use elements.values().get(0)
. That way you can remove the unnecessary loop and throw new IllegalStateException("This exception should never be thrown");
from elementary.
For the
single()
implementation you can also useelements.values().get(0)
. That way you can remove the unnecessary loop andthrow new IllegalStateException("This exception should never be thrown");
element.values()
returns a Collection
which doesn't have a get(index)
method. Although now that you mention it, I could convert it into an array and retrieve the single element.
Also, you could use the TestAbortedException (which is also used by Junit Jupiter uses for test assumptions), instead of the IllegalStateExceptions
I did think about it, but I figured an IllegalStateException
works just as well without having to include an additional dependency.
from elementary.
Hearing no other objections, I'll be going ahead with the proposed changes and finalizing them.
from elementary.
I do think @Case
is an appropriate name though since I do think it's similar in concept to that of case distinction. In my mind, we might want to test a single component/lint against various different configurations/cases. In a sense, there are sort of test cases.
For example, we might want to test the behavior of a lint in different cases to ensure that there are no false positive/negatives:
@Case("valid")
void foo(String param) {}
@Case("missing parameter")
void foo() {}
@Case("has return type")
String void(String param)
At least, this is how I envisioned the @Case
annotation to be used.
On another note, @Element
won't be a good choice since it'll conflict with Java's own Element
type.
from elementary.
Took me a bit to re-read the article and Case.java
, but I have a somewhat clearer picture now indeed, and yes it's clear that @Case
was intended to be something like a switch label like in a Java switch
statement.
It can be used for other purposes as well.
E.g. I can imagine situations where I want to write a test that accesses some attribute, e.g. to pick up its name to double-check that generated getter/setter/wither names are consistent with the original name despite attached prefixes and suffixes.
Or e.g. the annotation processor will accept overrides and not generate code provided, say, there's a setter with the right name and parameter type:
@Bean
private class BeanTemplate // Not necessarily code that's useful at runtime, just a spec for the annotation processor
@Case("attribute") private int attr;
private boolean optional;
@Case("correct_setter_parameters") public void setAttr(int attr) {
this.attr = attr;
}
@Case("wrong parameter type on setter") public void setAttr(int attr) {
this.attr = attr;
}
@Case("too many parameters on setter") public void setAttr(int attr, boolean optional) {
this.attr = attr;
this.optional = optional;
}
@Case("no parameter on setter") public void setAttr() {
this.attr = "NO VALUE";
}
}
@Case("correct_setter_parameters")
, @Case("wrong parameter type on setter")
, @Case("too many parameters on setter")
, and @Case("no parameter on setter")
are indeed case labels, but @Case("attribute")
is just a tag so the test can conveniently pick up the attribute name and type from the test in case somebody refactors the example code and renames the attribute.
from elementary.
If I understand the example correctly, there's two distinct use-cases for @Case
,
- something similar to a
case
in Java's switch statements - labeling additional properties that can be accessed in tests
I think an interesting question is whether we should generalize both use-cases under a new single annotation or split them into two distinct annotations.
If we're going with generalizing the annotation, perhaps @Label
, @CodeSnippet
or @Snippet
?
from elementary.
I think yes, that's the use cases.
Though I'd say that the case
-analog @Case
s are just a specialization of the "we label code snippets" idea: A case label is, at least in C, an ordinary label, you can even goto
it... which is horribly unclean, of course, but it's the concept of a label.
I can see reasons for both @Label
and @Snippet
:
@Label
since the annotation labels an Element
.
@Snippet
because it's labelling an Element
, which is a code snippet. (hmm... @Snippet
is a bit less accurate because people generally consider multiple Element
s a snippet - e.g. two statements can be a snippet but you can't label them with one annotation, you have to put them into a pair of braces.)
It's still somewhat subjective; I have a slight preference for @Label
.
The fully spelled-out name would be @ElementLabel
since the annotation labels an Element
, but that's probably too long and unwieldy.
Name conflict analysis:
java.awt.Label
. Unlikely to ever be used in modern code.java.jfr.Label
. Java Flight Recorder. Not sure if anybody is ever going to write an annotation processor test for that.- Various
jdk.internal.
andsun.
packages. That's internal, do we need to consider that?
from elementary.
Hmmm, seems like @Label
is a better candidate then, since looking at it, people may mistakenly assume that @Snippet
spans multiple lines/elements.
I don't think most of the name conflicts matter since they belong to quite the niche areas. I daresay @Label
will work fine in roughly 90% of all scenarios.
I'll probably rename @Case
to @Label
on master in that case. It'll probably take awhile to make it to stable though since there's quite significant rework being done to the satisfactory
project.
from elementary.
Regarding satisfactory
- judging by the class names, it seems to be very similar to what Hamcrest does.
If that wild guess happens to be correct, it might be a good idea to leave the composability to Hamcrest and merely provide basic predicates like "this is a type".
(Just an idea. I know ideas are cheap :-) )
from elementary.
It's similar that they are matchers but I think that's where the similarity with Hamcrest
ends. The idea is to be able to compose matchers for various elements in a declarative manner (as opposed to how most processors imperatively check types).
Those matchers then can automatically produce error messages using Utilitary
's message formatting.
The message formatting & matching parts are done, I just need to figure out how to create nice error messages based on the matchers. This part's taking quite awhile to sort out.
from elementary.
Ah ok then.
Given that not everybody will want to learn how to integrate it, or how to write messages that interact nicely, it would be good if elementary did not depend on satisfactory, i.e. they should either be independent, or satisfactory should depend on elementary. (I know what such decoupling creates work, sometimes in considerable amounts, so I'll understand if you don't want to do this.)
from elementary.
Hmm I'll probably look into splitting it up once I have some spare time. Unfortunately it probably won't happen until at least November.
from elementary.
I feel like this mixes 2 features:
- The ability to get references to specific types of elements that will be used by the annotation processor
- The ability to easily switch over different elements on a case by case basis
Just renaming the @Case
annotation wouldn't fully encompass what you want to achieve. Therefore I propose the following:
@Element
annotation with the following annotation values:
label
this is to define the name with which to retrieve the element in your test (providing this is required)case
in case that you want multiple different test cases, one for each test case, similar to a@ParameterizedTest
(has a default value)value
to make it possible to provide a name like@Element("myAwesomeLabel")
instead of having to write@Element(label = "myAwesomeLabel")
(solabel
is basically an alias forvalue
)
Let's give an example of how this would be used. Here is the example from the The Problem With Annotation Processors article:
import com.karuslabs.elementary.junit.Cases;
import com.karuslabs.elementary.junit.Tools;
import com.karuslabs.elementary.junit.ToolsExtension;
import com.karuslabs.elementary.junit.annotations.Inline;
import com.karuslabs.utilitary.type.TypeMirrors;
@ExtendWith(ToolsExtension.class)
@Inline(name = "Samples", source = {
"import com.karuslabs.elementary.junit.annotations.Case;",
"",
"class Samples {",
" @Case(\"first\") String first;",
" @Case String second() { return \"\";}",
"}"})
class ToolsExtensionExampleTest {
Lint lint = new Lint(Tools.typeMirrors());
@Test
void lint_string_variable(Cases cases) {
var first = cases.one("first");
assertTrue(lint.lint(first));
}
@Test
void lint_method_that_returns_string(Cases cases) {
var second = cases.get(1);
assertFalse(lint.lint(second));
}
}
Lets rewrite this with the @Element
annotation and add a case:
import com.karuslabs.elementary.junit.Tools;
import com.karuslabs.elementary.junit.ToolsExtension;
import com.karuslabs.elementary.junit.annotations.Inline;
import com.karuslabs.utilitary.type.TypeMirrors;
import javax.lang.model.element.Element;
@ExtendWith(ToolsExtension.class)
@Inline(name = "Samples", source = {
"import com.karuslabs.elementary.junit.annotations.Element;",
"",
"class Samples {",
" @Element(\"first\") String first;",
" @Element(label = \"second\", case = \"no param\") String second() { return \"\";}",
" @Element(label = \"second\", case = \"one param\") String second(String param) { return \"\";}",
" @Element(label = \"second\", case = \"two params\") String second(String param1, String param2) { return \"\";}",
"}"})
class ToolsExtensionExampleTest {
Lint lint = new Lint(Tools.typeMirrors());
@Test
void lint_string_variable(Element first) {
assertTrue(lint.lint(first));
}
@Test
void lint_method_that_returns_string(Element second) {
assertFalse(lint.lint(second));
}
}
This would run 4 tests: lint_string_variable()
will run once and lint_method_that_returns_string()
will run 3 times (once for each case). For the 3 test cases of lint_method_that_returns_string()
, the case
string will be used as the name of that test, grouped under the test method name, similar to parameterized tests.
You could still use the Cases
test method parameter as well (maybe renamed to Elements
or something like that), to be more explicit about what Element you wish to fetch and if you want al test cases to be tested within a single test method call (if that's their personal preference or something). It could also be used to get access to the specific case
string from the annotation.
from elementary.
For completeness, here is how the test would look with the Cases
/Elements
test method parameter:
import com.karuslabs.elementary.junit.Tools;
import com.karuslabs.elementary.junit.ToolsExtension;
import com.karuslabs.elementary.junit.annotations.Inline;
import com.karuslabs.utilitary.type.TypeMirrors;
import javax.lang.model.element.Element;
import java.util.List;
import java.util.Map;
@ExtendWith(ToolsExtension.class)
@Inline(name = "Samples", source = {
"import com.karuslabs.elementary.junit.annotations.Element;",
"",
"class Samples {",
" @Element(\"first\") String first;",
" @Element(label = \"second\", case = \"no param\") String second() { return \"\";}",
" @Element(label = \"second\", case = \"one param\") String second(String param) { return \"\";}",
" @Element(label = \"second\", case = \"two params\") String second(String param1, String param2) { return \"\";}",
"}"})
class ToolsExtensionExampleTest {
Lint lint = new Lint(Tools.typeMirrors());
@Test
void lint_string_variable(Elements elements) {
// maybe it would be better if the one(String label) method was renamed to get(String label) and threw a TestAbortedException (the same one that junit5 assumptions throw) rather than returning null if there isn't exactly one element
Element first = elements.get("first");
assertTrue(lint.lint(first));
}
@Test
void lint_method_that_returns_string(Elements elements) {
// without case string
List<Element> secondList = elements.asList("second");
secondList.foreach(second -> {
assertFalse(lint.lint(second));
}
// with case string
Map<String, Element> secondMap = elements.asMap("second");
secondList.foreach((case, second) -> {
assertFalse(lint.lint(second), "For case: " + case");
}
}
}
from elementary.
I also think that Cases.get(int index)
can be removed if needed, as it seems very error-prone to retrieve elements without label, especially if you retrieve them from a possibly nonexistent index.
If you do still want the functionality, you can always use Elements.asList("myLabel").get(1);
from elementary.
I feel like both cases can be generalised as “get elements annotated with a label”.
I’m not too particularly fond of cases for parameterised tests as it isn’t obvious from a glance that a test is parameterised since it’s not annotated with @ParameterizedTest
. People not familiar with elementary might be caught off-guard by this. I’m also not too sure if it’s possible to re-execute a test marked with @Test
but with different parameters each time.
I feel a simpler way will just be to mark a test with @ParameterizedTest
and a list of labels instead. If that doesn’t work at the moment, then I think effort should be in this direction instead. Furthermore, it’s less surprising.
I don’t think renaming Case
And Cases
to Element
and Element
s is a good idea since they will conflict with Java’s own TypeMirror API. @Label and Labels might be better.
Re-thinking about it, I agree that Cases.get(index) should be removed since it’s extremely confusing & error-prone.
Huh, TIL that TestAbortedException
existed, I agree that the method should definitely throw the exception rather than returning null.
Hmm regarding renaming one() to get(), perhaps it will be better to enforce that all labels are unique & maybe provide some forming of matching on that instead.
from elementary.
from elementary.
Would one still need "case" to make parameterized test cases?
Is "case" needed for nonparameterized test cases, does it even make sense?
case doesn't make sense for non-parameterized test cases, so that would mean that you probably would want a difference between @Label
and @CaseLabel
, where the latter is only used in parameterized tests. It would be possible to make the case argument optional, meaning that the cases would just have an index. You would just have to think about what to do when case labels with and without case argument are mixed. Do you want to disallow that or not, and if not, how would you handle that.
I think that focussing on the @Label
annotation at first would be more important though and if we want elementary to support parameterized tests later, it can always be added then.
Thinking about it though, we might be missing the point here. What are we trying to achieve? All we want is to have access to a certain Element object that has a certain structure. The way we get it doesn't really matter, so long as it is possible to retrieve with the given workaround. Sometimes it would be enough to just have the element for a field declaration or a method declaration, so having to specify the full class around it would be boilerplate that can be added automatically.
import com.karuslabs.elementary.junit.TestElements;
import com.karuslabs.elementary.junit.TestField;
import com.karuslabs.elementary.junit.TestMethod;
import com.karuslabs.elementary.junit.TestClass;
import com.karuslabs.elementary.junit.Tools;
import com.karuslabs.elementary.junit.ToolsExtension;
import com.karuslabs.utilitary.type.TypeMirrors;
import javax.lang.model.element.Element;
@ExtendWith(ToolsExtension.class)
class ToolsExtensionExampleTest {
Lint lint = new Lint(Tools.typeMirrors());
@Test
@TestFieldElement(label = "thatOneProblematicField", element = "private final @Foo String myPrivateAnnotatedStringField;", imports = {"my.package.Foo"}),
void lint_string_variable(TestElements elements) {
Element field = elements.get("thatOneProblematicField");
assertTrue(lint.lint(field));
}
@Test
@TestMethodElement(label = "mainMethod", element = "public static void main(String[] args){return null;}")
@TestClassElement(label = "myGenericClass",
element = "public class Foo<T extends Bar> implements Baz<T> {}",
imports = {"my.package.Foo", "my.package.Bar", "my.otherpackage.Baz"})
void lint_main_method_and_class(TestElements elements) {
Element method = elements.get("mainMethod");
Element clazz = elements.get("myGenericClass");
assertTrue(lint.lint(field));
doSomethingWith(clazz);
}
}
Given the specific annotation, we know exactly how to generate the boilerplate around the element (if the imports are defined correctly), so we don't need the tester to deal with that.
from elementary.
I disagree since doing static code analysis in an annotation processor will almost always rely on the Element/TypeMirror API, which IMO is quite a common use-case.
A @CaseLabelSource
does sound promising, perhaps we can go with the original idea of having a case/group field on the @Label
annotation.
from elementary.
I disagree since doing static code analysis in an annotation processor will almost always rely on the Element/TypeMirror API, which IMO is quite a common use-case.
I do agree that the Element/TypeMirror api is a common use case. What I'm trying to achieve here is actually to still have the class definition be generated behind the scenes, so that the Element objects are available for your test. This will prevent the user from having to write the boilerplate around the element of interest and skip the parts that aren't directly used in the test (eventhough they will still be available when compiling and creating the Element objects).
from elementary.
I think there's a misunderstanding, I just meant that I specifically disagreed with renaming the annotation to @Element
. I'm still on the fence regarding the others.
from elementary.
Ah ok. The annotation processor does indeed heavily rely on Element, but the code that actually would be given the @Element
annotation wouldn't usually rely on Element, right? As this would be the compilation target.
from elementary.
I think the main issue is that during the test we want access to both Java's Element
and the @Element
annotation.
Sometimes it would be enough to just have the element for a field declaration or a method declaration, so having to specify the full class around it would be boilerplate that can be added automatically.
Re-reading this it seems like I missed out this part, I think we already have a far superior mechanism for eliminating boilerplate code, the @Introspect
annotation, it allows us to declare @Case
s in the test class itself.
@ExtendWith(ToolsExtension.class)
@Introspect
class ToolsExtensionExampleTest {
Lint lint = new Lint(Tools.typeMirrors());
@Test
void lint_string_variable(Cases cases) {
var first = cases.one("first");
assertTrue(lint.lint(first));
}
@Case("first") String foo() { return "";}
}
class Lint {
final TypeMirrors types;
final TypeMirror expectedType;
Lint(TypeMirrors types) {
this.types = types;
this.expectedType = types.type(String.class);
}
public boolean lint(Element element) {
if (!(element instanceof VariableElement)) {
return false;
}
var variable = (VariableElement) element;
return types.isSameType(expectedType, variable.asType());
}
}
This feature does not benefit from renaming @Case
to @Element
.
Perhaps another issue at hand here is the discoverability of @Introspect
?
from elementary.
I created a rough prototype here. Do let me know your thoughts!
@Case
was renamed to @Label
. One important difference is that all labels must now have a unique value. This differs from @Case
where a value was neither required nor unique. Additionally, a new group
attribute was introduced to @Label
. This allows us to retrieve all @Labels
in a group. Groups are expected to tie-in nicely with support for parameterized testing further down the line.
Cases
have been reworked into Labels
. Accessing values by index has always been error-prone and hence no longer supported.
A few nice QOL changes have been made to it, including the addition of single()
. I'm not too sure what other ergonomic changes are missing. Feedback on it is greatly appreciated. In general, I tried to avoid anything related to sequencing/ordering since the order of elements is highly likely to be subject to frequent changes.
from elementary.
Perhaps another issue at hand here is the discoverability of
@Introspect
?
Ah while I had seen @Introspect
, I hadn't looked into what it does. Most of my knowledge comes from this article.
Looking at the wiki, I see that it is described there. The only thing I'd add there is a code example, but other than that it's fine.
But taking into account @Introspect
, I agree that @Element
isn't a good name due to the naming conflict and that @Label
is the better choice here.
I like the label changes
from elementary.
Also, you could use the TestAbortedException (which is also used by Junit Jupiter uses for test assumptions), instead of the IllegalStateExceptions
from elementary.
Fair. While I believe that the dependency is included with Junit Jupiter, it is always good to not rely on transitive dependencies and to use as few dependencies as possible when writing a library, to prevent dependency conflicts, so I agree with that choice.
from elementary.
If possible(!), keep the old annotations and Javadoc them as @deprecated, explaining how to migrate to the new system.
from elementary.
We're planning to include this in 2.0.0, which probably will be a clean break.
from elementary.
In that case it would be nice if you added @Deprecated(forRemoval=true)
in version 1.2.0
from elementary.
I thought a little further about it and I think I'm going to leave only the @Case
in 2.0.0 marked as deprecated for removal. It'll point to a migration doc that I'll be writing. If only we had the tooling to automatically migrate it over to the new annotation :(.
from elementary.
You could write an annotation processor that generates a file with updated code for copy&paste ;-)
Not sure if this is a good idea or Just Too Much Work, just an idea that popped up on my mind.
More seriously, Elementary could simply continue supporting @Case
.
The typical behaviour for long-lived code is to keep such stuff until the next major revision (that would be 3.x).
Stuff in really wide-spread use (such as Java itself, or Maven) actually keeps it indefinitely.
(Gradle and MongoDB are exceptions, and people heavily dislike losing features, even wen it's a major revision.)
from elementary.
I really want to create an automated tool to do it but I just can't justify the effort for it sadly.
Thinking about it, I would honestly prefer a clean break between major version. My thought process behind it is that it's less maintenance burden. Also (to my knowledge at least), Elementary isn't widely used and I think it's fine and we should take advantage while we can to make breaking changes. People can always stay on 1.X
from elementary.
I opened a PR outlining all the changes to @Case
and related APIs. If there's no issues with it, I'll be merging it in during the weekends.
from elementary.
Fixed in #164
from elementary.
Related Issues (18)
- Your .dependabot/config.yml contained invalid details HOT 1
- Missing page "Why java 11?" HOT 4
- [BUG] Repo missing in elementary/README.md HOT 9
- [BUG] Cannot use source with intentional compile errors for ToolsExtension HOT 10
- [FEATURE REQUEST] First-class module support in JUnit extensions HOT 7
- [BUG] `MemoryFileManager ` does not handle modules correctly
- [FEATURE REQUEST] Better support for parameterized tests HOT 1
- [BUG] Modules don't compile HOT 1
- Deploy artifacts to maven central HOT 12
- Dependency Dashboard
- [FEATURE REQUEST] Auto-format pull requests
- [FEATURE REQUEST] Primitives for Module support HOT 7
- [BUG] com.karuslabs:satisfactory 1.1.3 is not available HOT 2
- Requires `org.junit.jupiter:junit-jupiter-params`.
- Documentation lacks sufficient setup information to prevent missing class error. HOT 5
- Documentation examples require Utilitary. HOT 1
- [BUG] `java.nio.file.FileSystemNotFoundException: Provider "mem" not installed` HOT 7
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from elementary.