Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test runner indicators #4737

Merged
merged 6 commits into from
Mar 22, 2025
Merged

Conversation

HollandDM
Copy link
Contributor

@HollandDM HollandDM commented Mar 16, 2025

/claim #4735

This PR add some calculation to add test progress & failure indicator for each test runner while testParallelism is set to true

Screenshot 2025-03-16 at 23 50 22

To add progress indicators, this pull request introduces a new file within each worker's claim folder. This file is overwritten every time runClaimedTestClass is called. The parent process will checks these files in each worker, accumulates their contents, and outputs them via a ticker.

For test timing, this pull request uses a separate map, similar to workerStatusMap, to store the starting time. The time calculation is then straightforward, and the result is output via a ticker alongside the test class name.

Comment on lines 365 to 366
@internal private[TestRunnerUtils] final class TestEventSummary(outputFileOpt: Option[os.Path])
extends AtomicBoolean(true) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class wrapper and inheritance is kind of weird. Let's convert them to just static helper methods and pass in outputFileOpt and the AtomicBoolean as arguments where necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a trick to better the memory layout of object, I guess it's kind of overkill here

Comment on lines 378 to 380
val (success, failure) = upickle.default.read[(Long, Long)](os.read.stream(outputFile))
val (newSuccess, newFailure) =
if (isSuccess) (success + 1, failure) else (success, failure + 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow to both read and write to the file on disk is unnecessarily complicated. Let's just keep the success/failure count as in-memory Long (or AtomicLong) and have the file be write-only purely for the purpose of passing the information to the parent process

Comment on lines 415 to 418
// help gc
workerStatusMap.clear()
workerStatsSet.clear()
testClassTimeMap.clear()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be unnecessary, let's skip it

val result = callTestRunnerSubprocess(
base,
Right((startingTestClass, testClassQueueFolder, claimFolder))
)
workerStatusMap.remove(claimLog)
// We don't remove workerStatsSet entry, as we still need them for calculation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can elaborate a bit more ...for calculation of total success/failure counts

@lihaoyi
Copy link
Member

lihaoyi commented Mar 22, 2025

@HollandDM Looks pretty good, some small changes requested. Please also update the PR description with a summary of your implementation strategy

@HollandDM HollandDM force-pushed the test-runner-indicators branch from 2ce3ad7 to 0e2ce69 Compare March 22, 2025 05:18
@HollandDM HollandDM requested a review from lihaoyi March 22, 2025 05:18
@HollandDM
Copy link
Contributor Author

Updated to adrees the feedbacks

event.status match {
case Status.Error => taskStatus.set(false)
case Status.Failure => taskStatus.set(false)
case _ => taskStatus.compareAndSet(true, true) // consider success as a default
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can leave out this compareAndSet; this branch of the match being a no-op would be sufficient

@@ -225,6 +225,8 @@ private final class TestModuleUtil(
)(implicit ctx: mill.api.Ctx) = {

val workerStatusMap = new java.util.concurrent.ConcurrentHashMap[os.Path, String => Unit]()
val workerStatsSet = new java.util.concurrent.ConcurrentHashMap[os.Path, Unit]()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call this workerResultsSet? Stats and Status sounds too similar

Comment on lines 379 to 382
val suffix = ((now - last) / 1000).toInt match {
case 0 => ""
case n => s" ${n}s"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move PromptLoggerUtil.renderSecondsSuffix to mill.internal.Util so we can re-use it here

@HollandDM HollandDM force-pushed the test-runner-indicators branch from e704d40 to 88d8467 Compare March 22, 2025 06:03
@HollandDM HollandDM requested a review from lihaoyi March 22, 2025 06:03
@HollandDM
Copy link
Contributor Author

Updated again. After the ticket is good to merge, I'll squash them back into 1 commit, I think it'll be better

@lihaoyi
Copy link
Member

lihaoyi commented Mar 22, 2025

I think this looks good for testParallelism = true. Could you also make the progress count and failure count show in testParallelism = false? I think a lot of code would be re-usable, and it would help improve the UX of the 0.12.x branch where test parallelism is false by default

@lihaoyi
Copy link
Member

lihaoyi commented Mar 22, 2025

No need to worry about squashing your commits, we'll squash it during the merge using github's squash-and-merge button

totalFailure += failure
}
ctx.log.ticker(
s"${totalSuccess + totalFailure}/${filteredClassCount} completed, ${totalFailure} failures"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's only print n failures for n > 0, since that's what we do in the main prompt for failed tasks

@HollandDM HollandDM force-pushed the test-runner-indicators branch from 053cd80 to a2aab21 Compare March 22, 2025 08:37
@HollandDM HollandDM requested a review from lihaoyi March 22, 2025 08:37
@HollandDM
Copy link
Contributor Author

Updated, I added progress indicator (complete/failure) to default run as well.

@HollandDM HollandDM force-pushed the test-runner-indicators branch from a2aab21 to 0810718 Compare March 22, 2025 08:39
@lihaoyi lihaoyi marked this pull request as ready for review March 22, 2025 09:04
Comment on lines 189 to 194
try {
// Periodically check the result log file and tick the relevant infos
executor.scheduleWithFixedDelay(
() => {
val (totalSuccess, totalFailure) = TestModuleUtil.calculateTestProgress(workerResultSet)
ctx.log.ticker(s"${totalSuccess + totalFailure}/${filteredClassCount} completed${
if totalFailure > 0 then s", ${totalFailure} failures" else ""
}")
},
0,
20,
java.util.concurrent.TimeUnit.MILLISECONDS
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's consolidate the whole val workerResultSet, newScheduledThreadPool, scheduleWithFixedDelay, finally executor.shutdown logic into a single

def withTestProgressTickerThread(updateWorkerStatuses: Boolean)(block: => T): T

That should help DRY up the duplicate code, and also shorten some of these def runTestQueueScheduler/def runTestDefault methods which are getting rather long

@@ -203,15 +211,34 @@ import java.io.PrintStream
}

def runTasks(
tasks: Seq[Task],
tasksSeq: Seq[Seq[Task]],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this change in signature from Seq[Task] to Seq[Seq[Task]]? I've been looking at the code and haven't managed to figure out the intention

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess each element of the outer Seq represents one test class, and each inner Seq represents the tasks for that test class? If so, probably worth a comment somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm adding the comments in the backport PR, will add it in this as well. But it's meaning is exactly what you've said

@lihaoyi
Copy link
Member

lihaoyi commented Mar 22, 2025

Some last nitpicks. Tried it out locally and works great. Turned on CI, once it's green it should be good to merge

@lihaoyi
Copy link
Member

lihaoyi commented Mar 22, 2025

The mill.scalalib.TestRunnerTests.doneMessage.failure seems to happen reliably on my laptop as well, might need to tweak the test or code to make it pass

@HollandDM HollandDM force-pushed the test-runner-indicators branch from 09fc0c4 to 0e1dfa3 Compare March 22, 2025 13:58
@HollandDM HollandDM requested a review from lihaoyi March 22, 2025 13:58
@HollandDM
Copy link
Contributor Author

updated, the test failing is due to the changes of Seq[Task] to Seq[Seq[Task]], I left a comment there

Comment on lines +300 to 308
val resultPath = base / s"result.log"
os.write.over(resultPath, upickle.default.write((0L, 0L)))
workerResultSet.put(resultPath, ())

val result = callTestRunnerSubprocess(
base,
resultPath,
Right((startingTestClass, testClassQueueFolder, claimFolder))
)
Copy link
Member

@lihaoyi lihaoyi Mar 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consolidate this logic to use runTestRunnerSubprocess as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runTestRunnerSubprocess's are quite different between queueRunner and defaultRunner, so I have to create 2 versions for it, each in the corresponding runners function.
I'm also thinking of a way to further clean up the logic, if we can generalize both runners to just produce blueprint for subprocess, and do the spawn + logging in a shared function, then the code should be clearer. But that required some refactoring.
If you want, I can try to go that way and see if we can do any better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I think for now let's just leave it as is then

@lihaoyi lihaoyi merged commit 6fd66af into com-lihaoyi:main Mar 22, 2025
61 of 63 checks passed
lihaoyi pushed a commit that referenced this pull request Mar 22, 2025
back port of #4737

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
@lihaoyi
Copy link
Member

lihaoyi commented Mar 22, 2025

Thanks @HollandDM , i will send the bounty using your existing transfer details

@HollandDM
Copy link
Contributor Author

Thanks @lihaoyi, lets use that as my transfer detail

@lefou lefou added this to the 0.13.0 milestone Mar 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants