Skip to content

Commit 31d5d88

Browse files
committed
Tweaks to the artifacts for ECS runner
1 parent 4abf69d commit 31d5d88

4 files changed

Lines changed: 193 additions & 30 deletions

File tree

docs/submission-phase-scoring.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ Runner task logs are written to CloudWatch using the submission runner log strea
101101

102102
For `PROVISIONAL` and `SYSTEM` scoring, the runner writes progress to the phase review summation metadata through `POST /v6/marathon-match/internal/scoring-progress`. Review API exposes this as `reviewSummation.metadata.testProcess` (`provisional` or `system`), `reviewSummation.metadata.testProgress` (`0` to `1`), and `reviewSummation.metadata.testStatus` (`IN PROGRESS`, `SUCCESS`, or `FAILED`) when metadata is included in the response. In-progress summations keep a neutral placeholder score and must be rendered as unavailable based on `testStatus`; only `FAILED` progress uses the failed-score sentinel. SYSTEM timeout failures also include `reviewSummation.metadata.timed_out = true`.
103103

104+
For generic runner artifacts, the public provisional/system `output.txt` includes testcase ordinal, runtime, runner/tester errors, and submitted-solution stderr, but excludes submitted-solution stdout and raw per-test scores. Per-test score details remain in the private internal `reviews.json` artifact and callback metadata.
105+
104106
When more than one phase config matches the currently open challenge phases, the handler launches one scorer task per match. This is the supported way to run both `EXAMPLE` and `PROVISIONAL` scoring from the same Submission phase.
105107

106108
## Relative scoring

ecs-runner/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ Once this is saved and the config is active, new scoring launches use the new EC
141141

142142
The runner rejects output once generated public `output.txt` content or a public/private artifact archive would exceed `MM_RUNNER_MAX_OUTPUT_BYTES`. The default is `10000000` bytes. The trusted parent runner passes the resolved limit into the isolated tester child, so generated generic-runner output and uploaded artifact zips use the same cap.
143143

144+
Generic runner public `output.txt` entries show testcase ordinal, runtime, runner/tester errors, and submitted-solution stderr. They intentionally omit submitted-solution stdout and raw per-test scores. The private internal artifact (`reviews.json` in the `*-internal` zip) carries the per-test `testScores` used for diagnostics and score verification.
145+
144146
## Local smoke test
145147

146148
```bash

ecs-runner/src/main/java/com/topcoder/runner/EcsRunnerMain.java

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3023,6 +3023,8 @@ private static Path ensurePrivateArtifactsDir(Path submissionDir) throws IOExcep
30233023

30243024
/**
30253025
* Builds the JSON payload stored in the internal {@code reviews.json} artifact.
3026+
* The payload includes sanitized top-level {@code testScores} so private
3027+
* consumers can inspect per-test scores even though public artifacts hide them.
30263028
*
30273029
* @param submissionId Submission whose review payload is archived.
30283030
* @param testPhase Scoring phase represented by the payload.
@@ -3066,12 +3068,42 @@ private static Map<String, Object> buildInternalReviewArtifactPayload(
30663068
payload.put("scorecardId", scorecardId);
30673069
payload.put("score", testerExecution.getScore());
30683070
payload.put("metadata", callbackMetadata);
3071+
payload.put(
3072+
"testScores",
3073+
buildInternalArtifactScoreEntries(testerExecution.getMetadata())
3074+
);
30693075
payload.put("currentReview", currentReview);
30703076
payload.put("impactedReviews", impactedReviews);
30713077
payload.put("reviews", reviews);
30723078
return payload;
30733079
}
30743080

3081+
/**
3082+
* Extracts per-test score entries for the private review artifact.
3083+
*
3084+
* <p>The member-visible public artifact intentionally omits per-test scores,
3085+
* but internal tooling still needs those values for diagnostics and score
3086+
* verification. Seed-bearing fields are removed so the private artifact keeps
3087+
* the same stable testcase identifiers used by callback metadata.
3088+
*
3089+
* @param metadata Structured tester metadata produced by generic or custom runner code.
3090+
* @return Sanitized per-test score entries, or an empty list when none were produced.
3091+
*/
3092+
private static List<Object> buildInternalArtifactScoreEntries(
3093+
Map<String, Object> metadata
3094+
) {
3095+
if (metadata == null) {
3096+
return new ArrayList<Object>();
3097+
}
3098+
3099+
Object testScores = metadata.get("testScores");
3100+
if (!(testScores instanceof List)) {
3101+
return new ArrayList<Object>();
3102+
}
3103+
3104+
return sanitizeMemberVisibleScoreEntries((List<?>) testScores);
3105+
}
3106+
30753107
/**
30763108
* Adds the runner's callback defaults to a tester-supplied current review payload.
30773109
*
@@ -4148,38 +4180,14 @@ private static TesterExecutionResult runGenericMarathonTester(
41484180
seedResult.put("error", seedError);
41494181
testScores.add(seedResult);
41504182

4151-
StringBuilder testOutputText = new StringBuilder();
4152-
testOutputText.append("Test Case #").append(testCaseNumber).append(":\n");
4153-
testOutputText.append("Score = ").append(seedScore).append('\n');
4154-
testOutputText.append("Run Time = ")
4155-
.append(testResult.getRunTime())
4156-
.append("ms\n");
4157-
if (
4158-
testResult.getError() != null
4159-
&& !testResult.getError().trim().isEmpty()
4160-
) {
4161-
testOutputText.append(testResult.getError().trim()).append('\n');
4162-
}
4163-
if (
4164-
testResult.getStdout() != null
4165-
&& !testResult.getStdout().trim().isEmpty()
4166-
) {
4167-
testOutputText.append("stdout:\n")
4168-
.append(testResult.getStdout().trim())
4169-
.append('\n');
4170-
}
4171-
if (
4172-
testResult.getStderr() != null
4173-
&& !testResult.getStderr().trim().isEmpty()
4174-
) {
4175-
testOutputText.append("stderr:\n")
4176-
.append(testResult.getStderr().trim())
4177-
.append('\n');
4178-
}
4179-
testOutputText.append('\n');
41804183
outputBytes = appendLimitedOutput(
41814184
outputText,
4182-
testOutputText.toString(),
4185+
buildMemberVisibleTestOutput(
4186+
testCaseNumber,
4187+
testResult.getRunTime(),
4188+
seedError,
4189+
testResult.getStderr()
4190+
),
41834191
outputBytes,
41844192
"artifacts/public/output.txt"
41854193
);
@@ -4241,6 +4249,40 @@ private static TesterExecutionResult runGenericMarathonTester(
42414249
}
42424250
}
42434251

4252+
/**
4253+
* Builds the public {@code output.txt} section for one generic Marathon test case.
4254+
*
4255+
* <p>The public provisional artifact shows runtime, tester errors, and stderr
4256+
* diagnostics, but it intentionally hides submitted-solution stdout and raw
4257+
* per-test scores. Per-test scores remain available in metadata and the
4258+
* private {@code reviews.json} artifact.
4259+
*
4260+
* @param testCaseNumber Stable 1-based testcase ordinal shown to members.
4261+
* @param runTimeMs Runtime reported by the Marathon tester for the testcase.
4262+
* @param error Tester or runner error text for the testcase.
4263+
* @param stderr Submitted solution stderr captured for the testcase.
4264+
* @return Member-visible text block ending with a blank line.
4265+
*/
4266+
private static String buildMemberVisibleTestOutput(
4267+
int testCaseNumber,
4268+
long runTimeMs,
4269+
String error,
4270+
String stderr
4271+
) {
4272+
StringBuilder testOutputText = new StringBuilder();
4273+
testOutputText.append("Test Case #").append(testCaseNumber).append(":\n");
4274+
testOutputText.append("Run Time = ").append(runTimeMs).append("ms\n");
4275+
4276+
if (error != null && !error.trim().isEmpty()) {
4277+
testOutputText.append(error.trim()).append('\n');
4278+
}
4279+
if (stderr != null && !stderr.trim().isEmpty()) {
4280+
testOutputText.append("stderr:\n").append(stderr.trim()).append('\n');
4281+
}
4282+
testOutputText.append('\n');
4283+
return testOutputText.toString();
4284+
}
4285+
42444286
/**
42454287
* Defers an isolated child temporary path until the child has emitted its
42464288
* final success or failure output.

ecs-runner/src/test/java/com/topcoder/runner/EcsRunnerMainTest.java

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package com.topcoder.runner;
22

3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
35
import static org.junit.Assert.assertNotNull;
46
import static org.junit.Assert.assertTrue;
57
import static org.junit.Assert.fail;
68

79
import java.io.OutputStream;
10+
import java.lang.reflect.Constructor;
811
import java.lang.reflect.Field;
912
import java.lang.reflect.InvocationTargetException;
1013
import java.lang.reflect.Method;
1114
import java.nio.file.Files;
1215
import java.nio.file.Path;
16+
import java.util.ArrayList;
1317
import java.util.Arrays;
18+
import java.util.LinkedHashMap;
19+
import java.util.List;
20+
import java.util.Map;
1421
import org.junit.Rule;
1522
import org.junit.Test;
1623
import org.junit.rules.TemporaryFolder;
@@ -49,6 +56,94 @@ public void createPublicArtifactZipRejectsOversizedOutput() throws Exception {
4956
}
5057
}
5158

59+
@Test
60+
public void buildMemberVisibleTestOutputHidesStdoutAndScoreButShowsStderr()
61+
throws Exception {
62+
Method method = EcsRunnerMain.class.getDeclaredMethod(
63+
"buildMemberVisibleTestOutput",
64+
int.class,
65+
long.class,
66+
String.class,
67+
String.class
68+
);
69+
method.setAccessible(true);
70+
71+
String output = (String) method.invoke(
72+
null,
73+
3,
74+
42L,
75+
"tester error",
76+
"solution stderr"
77+
);
78+
79+
assertTrue(output.contains("Test Case #3:"));
80+
assertTrue(output.contains("Run Time = 42ms"));
81+
assertTrue(output.contains("tester error"));
82+
assertTrue(output.contains("stderr:"));
83+
assertTrue(output.contains("solution stderr"));
84+
assertFalse(output.contains("Score ="));
85+
assertFalse(output.contains("stdout:"));
86+
}
87+
88+
@Test
89+
public void buildInternalReviewArtifactPayloadIncludesPerTestScores()
90+
throws Exception {
91+
Map<String, Object> scoreEntry = new LinkedHashMap<String, Object>();
92+
scoreEntry.put("testcase", "1");
93+
scoreEntry.put("seed", 12345L);
94+
scoreEntry.put("score", 98.5);
95+
scoreEntry.put("runTimeMs", 31L);
96+
97+
List<Map<String, Object>> testScores =
98+
new ArrayList<Map<String, Object>>();
99+
testScores.add(scoreEntry);
100+
101+
Map<String, Object> metadata = new LinkedHashMap<String, Object>();
102+
metadata.put("testScores", testScores);
103+
104+
Map<String, Object> currentReview = new LinkedHashMap<String, Object>();
105+
currentReview.put("metadata", metadata);
106+
107+
Object testerExecution = createTesterExecutionResult(
108+
98.5,
109+
metadata,
110+
currentReview
111+
);
112+
Method method = EcsRunnerMain.class.getDeclaredMethod(
113+
"buildInternalReviewArtifactPayload",
114+
String.class,
115+
String.class,
116+
String.class,
117+
String.class,
118+
getTesterExecutionResultClass(),
119+
Map.class
120+
);
121+
method.setAccessible(true);
122+
123+
@SuppressWarnings("unchecked")
124+
Map<String, Object> payload = (Map<String, Object>) method.invoke(
125+
null,
126+
"submission-id",
127+
"provisional",
128+
"review-type-id",
129+
"scorecard-id",
130+
testerExecution,
131+
metadata
132+
);
133+
134+
Object payloadScores = payload.get("testScores");
135+
assertTrue(payloadScores instanceof List);
136+
List<?> scoreList = (List<?>) payloadScores;
137+
assertEquals(1, scoreList.size());
138+
assertTrue(scoreList.get(0) instanceof Map);
139+
140+
Map<?, ?> internalScore = (Map<?, ?>) scoreList.get(0);
141+
assertEquals("1", internalScore.get("testcase"));
142+
assertEquals(98.5, ((Number) internalScore.get("score")).doubleValue(), 0.0);
143+
assertEquals(31L, ((Number) internalScore.get("runTimeMs")).longValue());
144+
assertFalse(internalScore.containsKey("seed"));
145+
}
146+
52147
private Path createArtifactsDir() throws Exception {
53148
Path artifactsDir = temporaryFolder.newFolder("artifacts").toPath();
54149
Files.createDirectories(artifactsDir.resolve("public"));
@@ -72,6 +167,28 @@ private Path invokeCreatePublicArtifactZip(Path artifactsDir) throws Exception {
72167
);
73168
}
74169

170+
private Object createTesterExecutionResult(
171+
double score,
172+
Map<String, Object> metadata,
173+
Map<String, Object> currentReview
174+
) throws Exception {
175+
Constructor<?> constructor = getTesterExecutionResultClass()
176+
.getDeclaredConstructor(double.class, Map.class, Map.class, List.class);
177+
constructor.setAccessible(true);
178+
return constructor.newInstance(
179+
score,
180+
metadata,
181+
currentReview,
182+
new ArrayList<Map<String, Object>>()
183+
);
184+
}
185+
186+
private Class<?> getTesterExecutionResultClass() throws Exception {
187+
return Class.forName(
188+
"com.topcoder.runner.EcsRunnerMain$TesterExecutionResult"
189+
);
190+
}
191+
75192
private long getMaxOutputBytes() throws Exception {
76193
Field field = EcsRunnerMain.class.getDeclaredField("MAX_OUTPUT_BYTES");
77194
field.setAccessible(true);

0 commit comments

Comments
 (0)