fix: Version from modrinth command error handling and logging - #854
fix: Version from modrinth command error handling and logging #854harrisonablack wants to merge 31 commits into
Conversation
Returns a list of all minecraft releases, excluding snapshots, betas / alphas
…esting Exposes url to override during testing
Produces duplicate logs as the ExceptionHandler will handle errors again after log
…instead of Generic Exception Generic exception handled by ExceptionHandler prints trace log, producing unneeded extra logs
- Class no longer exposes internal process gameVersionArgs - Added test to ensure logs contain information - Append existing tests with Psuedo MCAIPI json
|
Thanks for addressing these. Glancing at this I'm a bit worried about line count on the diff. Anything you can do to minimize that is good. |
Yeah i had a similar instinctual thought, around ~150 lines are from tests, and probably another ~100 from javadoc. I did update/touch pretty much all the core logic due to the way of logging, however if it is still too much i'm happy to revert the core structural changes and just focus on the retrofiting the fixes |
Thanks for confirming. I'll review and let you know. Is it about ready for a full review? |
harrisonablack
left a comment
There was a problem hiding this comment.
Ahh should be good, it's getting quite late for me now, but if you have a look over, i can come back tomorrow morning to fix anything up if needed!
itzg
left a comment
There was a problem hiding this comment.
Thanks for tightening up the algorithm. Just stylist changes needed.
| .withBody(String.format( | ||
| "[{\"game_versions\":[\"%s\"],\"id\":\"version-%s\",\"date_published\":\"2026-01-01T00:00:00Z\",\"version_type\":\"release\"}]", | ||
| gameVersion, | ||
| project | ||
| )) |
There was a problem hiding this comment.
I prefer to not build JSON string with placeholders. Just a little brittle and easy to miss quotes, etc. Since WireMock supports it, I prefer building dynamic JSON from node objects such as
| @SuppressWarnings("unused") // will be fixed https://youtrack.jetbrains.com/issue/IDEA-358214/Support-JUnit-5-FieldSource-annotation | ||
| static List<Arguments> processGameVersionsArgs = asList( | ||
| argumentSet("matches", asList( | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8") | ||
| ), "1.21.8" | ||
| ), | ||
| argumentSet("justOneOff", asList( | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7"), | ||
| asList("1.21.6", "1.21.7", "1.21.8") | ||
| ), "1.21.7" | ||
| ), | ||
| argumentSet("mismatch", asList( | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.4", "1.21.5"), | ||
| asList("1.21.6", "1.21.7", "1.21.8") | ||
| ), null | ||
| ), | ||
| argumentSet("fabric-api + nucledoom", asList( | ||
| // part of fabric-api | ||
| asList("24w46a", | ||
| "1.21.4-pre1", | ||
| "1.21.4-pre2", | ||
| "1.21.4-pre3", | ||
| "1.21.4-rc3", | ||
| "1.21.4", | ||
| "25w02a", | ||
| "25w03a", | ||
| "25w04a", | ||
| "25w05a", | ||
| "25w06a", | ||
| "25w07a", | ||
| "25w08a", | ||
| "25w09a", | ||
| "25w09b", | ||
| "25w10a", | ||
| "1.21.5-pre1", | ||
| "1.21.5-pre2", | ||
| "1.21.5-pre3", | ||
| "1.21.5-rc1", | ||
| "1.21.5-rc2", | ||
| "1.21.5", | ||
| "25w14craftmine", | ||
| "25w15a", | ||
| "25w16a", | ||
| "25w17a", | ||
| "25w18a", | ||
| "25w19a", | ||
| "25w20a", | ||
| "25w21a", | ||
| "1.21.6-pre1", | ||
| "1.21.6-pre3", | ||
| "1.21.6", | ||
| "1.21.7-rc1", | ||
| "1.21.7", | ||
| "1.21.8", | ||
| "25w31a", | ||
| "25w32a"), | ||
| // part of nucledoom | ||
| singletonList("1.21.4") | ||
| ), "1.21.4" | ||
| ) | ||
| ); |
There was a problem hiding this comment.
Due to the changes of the command, VersionFromModrinthProjectsCommand#processGameVersions was updated to process project refs alongside the List<List<String>>, this allowed logging and errors based on what projects are missing.
I will add back a set of test to cover the test regression.
| @Test | ||
| void rejectsEmptyProjects() { | ||
| final VersionFromModrinthProjectsCommand command = new VersionFromModrinthProjectsCommand(); | ||
| command.projects = List.of(); |
There was a problem hiding this comment.
Even with tests I don't like fields to be directly set. Even more so I prefer the CLI aspect to be re-tested in these various cases.
I know it's a whole lot more code, but do it like this
There was a problem hiding this comment.
I've replaced the test cases with test calling new CommandLine(new McImageHelper)).execute(args...)
| @BeforeEach | ||
| void startCapturingLogging() { | ||
| prevLevel = logger.getLevel(); | ||
| logger.setLevel(Level.DEBUG); | ||
| logAppender.start(); | ||
| logger.addAppender(logAppender); | ||
| } | ||
|
|
||
| @SuppressWarnings("unused") // will be fixed https://youtrack.jetbrains.com/issue/IDEA-358214/Support-JUnit-5-FieldSource-annotation | ||
| static List<Arguments> processGameVersionsArgs = asList( | ||
| argumentSet("matches", asList( | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8") | ||
| ), "1.21.8" | ||
| ), | ||
| argumentSet("justOneOff", asList( | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7"), | ||
| asList("1.21.6", "1.21.7", "1.21.8") | ||
| ), "1.21.7" | ||
| ), | ||
| argumentSet("mismatch", asList( | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.6", "1.21.7", "1.21.8"), | ||
| asList("1.21.4", "1.21.5"), | ||
| asList("1.21.6", "1.21.7", "1.21.8") | ||
| ), null | ||
| ), | ||
| argumentSet("fabric-api + nucledoom", asList( | ||
| // part of fabric-api | ||
| asList("24w46a", | ||
| "1.21.4-pre1", | ||
| "1.21.4-pre2", | ||
| "1.21.4-pre3", | ||
| "1.21.4-rc3", | ||
| "1.21.4", | ||
| "25w02a", | ||
| "25w03a", | ||
| "25w04a", | ||
| "25w05a", | ||
| "25w06a", | ||
| "25w07a", | ||
| "25w08a", | ||
| "25w09a", | ||
| "25w09b", | ||
| "25w10a", | ||
| "1.21.5-pre1", | ||
| "1.21.5-pre2", | ||
| "1.21.5-pre3", | ||
| "1.21.5-rc1", | ||
| "1.21.5-rc2", | ||
| "1.21.5", | ||
| "25w14craftmine", | ||
| "25w15a", | ||
| "25w16a", | ||
| "25w17a", | ||
| "25w18a", | ||
| "25w19a", | ||
| "25w20a", | ||
| "25w21a", | ||
| "1.21.6-pre1", | ||
| "1.21.6-pre3", | ||
| "1.21.6", | ||
| "1.21.7-rc1", | ||
| "1.21.7", | ||
| "1.21.8", | ||
| "25w31a", | ||
| "25w32a"), | ||
| // part of nucledoom | ||
| singletonList("1.21.4") | ||
| ), "1.21.4" | ||
| ) | ||
| ); | ||
| @AfterEach | ||
| void stopCapturingLogging() { | ||
| logger.detachAppender(logAppender); | ||
| logAppender.stop(); | ||
| logger.setLevel(prevLevel); | ||
| } |
There was a problem hiding this comment.
I use this for capturing output
https://github.com/stefanbirkner/system-lambda
and you can pass --debug via the normal command setup
There was a problem hiding this comment.
Ahhh, much nicer.
I've kept the @BeforeEach and @AfterEach to reset LogLevel. When running without it, debug log level is set and escapes the VersionFromModrinthProjectsCommandTest, it then breaks SyncAndInterpolateTest as the test assert stderr empty, however due to debug level, debug logs are sent to stderr
| final Map<String, Set<Integer>> supportedByVersion = new HashMap<>(); | ||
|
|
||
| for (int projectIndex = 0; projectIndex < versionMatrix.size(); projectIndex++) { | ||
| for (String gameVersion : new HashSet<>(versionMatrix.get(projectIndex))) { |
There was a problem hiding this comment.
The new HashMap threw me off. Using https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Set.html#copyOf(java.util.Collection) would be more readable.
Also, why is deduping needed here and not earlier, if at all?
There was a problem hiding this comment.
I realised after coming back after a few days, i was was overcomplicating this.
Initially when i was envisioning my approach, i planned a Map of each Minecraft version, against a set of supported projects; it doesn't make sense to allow duplicated of either. However deduping at this point in the process is silly, as we've already made api calls and performed logic.
I've added to the initial projects ref list pass a .distinct() to filter out duplicate entries.
final List<ProjectRef> refs = projects.stream()
.filter(Objects::nonNull)
.map(String::trim)
.filter(ref -> !ref.isEmpty())
.map(ProjectRef::parse)
.distinct()
.collect(Collectors.toList());I also realised the whole buildVersionMatrix approach which this comment is inside of was was overcomplicating everything. All the function did was invert a map of Projects -> Versions, to a map of Versions -> Projects. Removed that intermediary function.
|
I don't see new commits. Why did you mark the review comments as resolved? |
|
...speaking of which, it is standard practice for the reviewer to be the one that marks their review comments resolved and not the code author. It serves as a way to methodically close the review feedback loop. |
Unnecessarily built Map<String,Set<Integer>> Effectively inverting the data without providing any extra value, removed function which build matrix, and helper functions, updated wiring to take List<List<String>> input from versionFromProject
My apologies, i resolved the comments as an acknowledgement without requiring contest or clarification. I hadn't committed yet as i was still reviewing my local changes, as i felt the initial PR was quite brash.
Ahh, i see, my mistake. Thank you for the note, i wasn't aware of the convention.
I do agree this makes much more sense than what i initially thought. |
…ommandLine(McImageHelper).execute
And I forgot to mention the other half of my advice... as a code author I do like to keep track of which review comments I have looked at and agree without reply. Sometimes I'll use the thumbs up reaction that GitHub provides at the bottom of each comment. |
|
I haven't looked at the changes yet, but based on some of your follow up where you mention reducing complexity it got me wondering. When I first worked on this feature I found myself over complicating the algorithm and then found it was fairly concise in the end. The downside of that conciseness is that it seemed to limit to traceability back to the mismatching projects. I feel bad asking this, however I'm also a bit proud of my original implementation. Now that you know what you know, I'd be curious if you could look at my original code and see if there was a more direct way to get to the debugging output needed? |
This was the initial problem that prompted me to look at widening the breadth of what i was touching, it felt too awkward to retrofit the debugging into the existing algorithm
Please don't, half of my motivation for working on these issues bring out some humility in myself and learn a thing or two
I think the diff of this PR is way out of proportion of what it needs to be, and i did have a similar thought as well, that i was lulled by a sunken cost fallacy. Looking at it in retrospect, yes, there is a much more direct more direct way i've got which i will note shortly |
|
I created a seperate fresh branch for the change Within the original For example; Given the following We can create an During the first pass of the algorithm, we'd create the map; Which reads as; for minecraft 26.2, Project A, exists at index [0], of the [0] List, Project B exists at index [0] of the [1] list, and Project C doesn't support 26.2 Then for the second iteration we create another map. "26.1, [1,1,0]" For minecraft 26.1, project A exists at index [1] of list [0], Project B exists at index [1] of list [1], Project C exists at index [0] of list [2]. This way, we maintain a way to identify each project, before in the original algorithm, we were simply storing an integer score for each version, "how many projects does this version support", whereas now it maintains a seperate check per project, "Does the 0th project support this minecraft version?" For the following command, ./gradlew run --args="--debug version-from-modrinth-projects --loader=paper --projects=paper:viaversion,paper:viabackwards,paper:griefprevention,paper:discordsrv,paper:pl3xmap"It produces logs of which look like; I do want to add named logs, however will need to give the |
FWIW that is more self reflection I have heard than most of the companies / projects I have worked on. Executives never admit sunk cost fallacy 🤦♂️. |
|
Cool. The new approach looks and sounds really good. |
Reworked the internal logic of
VersionFromModrinthProjectsCommandto support #630 and #676.Version Blame #676
When a specific Modrinth project is blocking a release, a debug log is printed specifying the project that is blocking that version, e.g.
./gradlew run --args="--debug version-from-modrinth-projects --loader=paper --projects=paper:viaversion,paper:viabackwards,paper:griefprevention,paper:discordsrv,paper:pl3xmap:beta"Furthermore, if no compatible version is found, the same DEBUG logs are printed per version, as well as a final error;
Empty Projects and No Projects #630
When passing
--projects=with no data, an error is thrownIf no projects provided, another error is thrown.