Skip to content

Commit d5b38a2

Browse files
author
Julien Letrouit
authored
Merge pull request #45 from jletroui/infinite-loop
Detect and handles gracefully infinite loops and stack overflows
2 parents 5847eee + 2fd55c0 commit d5b38a2

4 files changed

Lines changed: 90 additions & 46 deletions

File tree

src/main/java/engine/ConsolePrinter.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,25 @@ public ConsolePrinter(Locale locale) {
1212

1313
@Override
1414
public void println() {
15-
System.out.println();
15+
synchronized(System.out) {
16+
System.out.println();
17+
System.out.flush();
18+
}
1619
}
1720

1821
@Override
1922
public void println(String template, Object... params) {
20-
System.out.println(String.format(template, params));
23+
synchronized(System.out) {
24+
System.out.println(String.format(template, params));
25+
System.out.flush();
26+
}
2127
}
2228

2329
@Override
2430
public void println(Localizable<String> template, Object... params) {
25-
System.out.println(String.format(template.get(locale), params));
31+
synchronized(System.out) {
32+
System.out.println(String.format(template.get(locale), params));
33+
System.out.flush();
34+
}
2635
}
2736
}

src/main/java/engine/Sensei.java

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import java.lang.reflect.InvocationTargetException;
44
import java.util.Arrays;
55
import java.util.List;
6+
import java.util.concurrent.atomic.AtomicBoolean;
67

78
import static engine.Texts.*;
89

910
/**
1011
* The main engine class, executing the series of koans.
1112
*/
1213
public class Sensei {
14+
private static final long TIMEOUT_INFINITE_LOOPS_MS = 2000;
1315
private final Locale locale;
1416
private final Printer consolePrinter;
1517
private Printer p = Printer.SILENT;
@@ -55,7 +57,8 @@ private boolean tryOffer(KoanTest test, int successfulCount) {
5557
if (!succeeded) {
5658
// If failed, execute verbosely the second time, in order to give feedback to the student.
5759
p = consolePrinter;
58-
return offer(test, successfulCount);
60+
offer(test, successfulCount);
61+
return false;
5962
}
6063

6164
return true;
@@ -66,53 +69,75 @@ private boolean offer(KoanTest test, int successfulCount) {
6669
observe(koan);
6770
encourage();
6871

69-
var success = false;
72+
AtomicBoolean success = new AtomicBoolean(false);
73+
74+
var thread = new Thread(() -> {
75+
try {
76+
success.set(executeCall(test));
77+
} catch (IllegalAccessException iae) {
78+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
79+
concludeConsole(koan);
80+
p.println(Color.red(EXPECTED_METHOD_TO_BE_PUBLIC), koan.methodName);
81+
} catch (IllegalArgumentException iae) {
82+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
83+
concludeConsole(koan);
84+
// Would be a bug in the Koan instances, since we are ensuring for the method with the right parameters.
85+
p.println(Color.red(THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR), koan.methodName, iae.getMessage());
86+
} catch (InvocationTargetException ite) {
87+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
88+
concludeConsole(koan);
89+
if (ite.getTargetException() instanceof StackOverflowError) {
90+
p.println(Color.red(THE_METHOD_SEEMS_TO_RECURSE_INFINITELY), koan.methodName);
91+
} else {
92+
p.println(Color.red(THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR), koan.methodName, ite.getCause().getMessage());
93+
}
94+
} catch (NoStaticMethodException nsme) {
95+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
96+
concludeConsole(koan);
97+
p.println(Color.red(EXPECTED_METHOD_TO_BE_STATIC), koan.methodName, koan.exerciseClassName(locale).replace(".", "/"));
98+
} catch (NoDynamicMethodException ndme) {
99+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
100+
concludeConsole(koan);
101+
p.println(Color.red(EXPECTED_METHOD_TO_NOT_BE_STATIC), koan.methodName, koan.exerciseClassName(locale));
102+
} catch (NoSuchConstructorException nsce) {
103+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
104+
concludeConsole(koan);
105+
displayConstructorNotFound(koan);
106+
} catch (NoSuchMethodException mnfe) {
107+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
108+
concludeConsole(koan);
109+
displayMethodNotFound(koan);
110+
} catch (ClassNotFoundException cnfe) {
111+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
112+
concludeConsole(koan);
113+
p.println(Color.red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE), koan.exerciseClassName.get(), koan.exerciseClassPackage.get());
114+
} catch (InstantiationException ie) {
115+
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
116+
concludeConsole(koan);
117+
// Would be a bug in the Koan instances, since we are ensuring for the method with the right parameters.
118+
p.println(Color.red(THE_CONSTRUCTOR_APPEARS_TO_PRODUCE_AN_ERROR), koan.exerciseClassName.get());
119+
}
120+
});
121+
122+
thread.setDaemon(true);
123+
thread.start();
70124
try {
71-
success = executeCall(test);
72-
} catch (IllegalAccessException iae) {
73-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
74-
concludeConsole(koan);
75-
p.println(Color.red(EXPECTED_METHOD_TO_BE_PUBLIC), koan.methodName);
76-
} catch (IllegalArgumentException iae) {
77-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
78-
concludeConsole(koan);
79-
// Would be a bug in the Koan instances, since we are ensuring for the method with the right parameters.
80-
p.println(Color.red(THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR), koan.methodName, iae.getMessage());
81-
} catch (InvocationTargetException ite) {
82-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
83-
concludeConsole(koan);
84-
p.println(Color.red(THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR), koan.methodName, ite.getCause().getMessage());
85-
} catch (NoStaticMethodException nsme) {
86-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
87-
concludeConsole(koan);
88-
p.println(Color.red(EXPECTED_METHOD_TO_BE_STATIC), koan.methodName, koan.exerciseClassName(locale).replace(".", "/"));
89-
} catch (NoDynamicMethodException ndme) {
90-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
91-
concludeConsole(koan);
92-
p.println(Color.red(EXPECTED_METHOD_TO_NOT_BE_STATIC), koan.methodName, koan.exerciseClassName(locale));
93-
} catch (NoSuchConstructorException nsce) {
94-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
95-
concludeConsole(koan);
96-
displayConstructorNotFound(koan);
97-
} catch (NoSuchMethodException mnfe) {
98-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
99-
concludeConsole(koan);
100-
displayMethodNotFound(koan);
101-
} catch (ClassNotFoundException cnfe) {
102-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
103-
concludeConsole(koan);
104-
p.println(Color.red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE), koan.exerciseClassName.get(), koan.exerciseClassPackage.get());
105-
} catch (InstantiationException ie) {
106-
// Special case: since the executeCall() method did not complete, the console conclusion was not displayed.
125+
thread.join(TIMEOUT_INFINITE_LOOPS_MS);
126+
}
127+
catch(InterruptedException ie) {
128+
throw new IllegalStateException("Something very weird happened. We should not have been interrupted.");
129+
}
130+
131+
if (thread.isAlive()) {
132+
StdStreamsInterceptor.reset();
107133
concludeConsole(koan);
108-
// Would be a bug in the Koan instances, since we are ensuring for the method with the right parameters.
109-
p.println(Color.red(THE_CONSTRUCTOR_APPEARS_TO_PRODUCE_AN_ERROR), koan.exerciseClassName.get());
134+
p.println(Color.red(THE_METHOD_SEEMS_TO_NOT_FINISH), koan.methodName);
110135
}
111136

112137
offerToMeditate(koan);
113138
showProgress(successfulCount);
114139

115-
return success;
140+
return success.get();
116141
}
117142

118143
private boolean executeCall(KoanTest test) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, InstantiationException {

src/main/java/engine/StdStreamsInterceptor.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ public static InterceptionResult capture(
106106
returnValue = executeFunc.run();
107107
}
108108
finally {
109-
System.setOut(realOut);
110-
System.setIn(realIn);
109+
reset();
111110
Helpers.cleanupStdInForKoan();
112111
printStream.close();
113112
}
@@ -118,4 +117,9 @@ public static InterceptionResult capture(
118117
returnValue
119118
);
120119
}
120+
121+
public static void reset() {
122+
System.setOut(realOut);
123+
System.setIn(realIn);
124+
}
121125
}

src/main/java/engine/Texts.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public class Texts {
3030
static Local<String> THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR =
3131
local("The method %s() appears to produce an error: %s.")
3232
.fr("La méthode %s() a produit une erreur: %s.");
33+
static Local<String> THE_METHOD_SEEMS_TO_NOT_FINISH =
34+
local("The method %s() appears to not finish. Did you code an infinite loop?")
35+
.fr("La méthode %s() semble ne jamais terminer. As tu codé une boucle infinie?");
36+
static Local<String> THE_METHOD_SEEMS_TO_RECURSE_INFINITELY =
37+
local("The method %s() appears to not finish. Did you call the method in itself, forming an infinite loop?")
38+
.fr("La méthode %s() semble ne jamais terminer. As tu appelé la méthode dans elle-même, formant une boucle infinie?");
3339
static Local<String> THE_CONSTRUCTOR_APPEARS_TO_PRODUCE_AN_ERROR =
3440
local("The constructor of %s appears to produce an error: %s.")
3541
.fr("Le constructeur de %s a produit une erreur: %s.");

0 commit comments

Comments
 (0)