Skip to content

Commit c6913d2

Browse files
committed
add tests for utils.Preferences
covers defaults loading, first-run creation, save/load round trips, the preferences.file override, platform-specific keys, backslash normalization, getInteger fallback, sorted save, the 3.x sketchbook migration, and the change listener contract. adds a package-private reset() for test isolation.
1 parent 803d647 commit c6913d2

2 files changed

Lines changed: 314 additions & 0 deletions

File tree

‎app/utils/src/main/java/processing/utils/Preferences.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,18 @@ static public void skipInit() {
222222
}
223223

224224

225+
/**
226+
* For testing, forget all state so the next test starts clean.
227+
*/
228+
static void reset() {
229+
table.clear();
230+
defaults = null;
231+
preferencesFile = null;
232+
initialized = false;
233+
listeners.clear();
234+
}
235+
236+
225237
/**
226238
* Check whether init() has been called. If not, we are probably not
227239
* running the full application.
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
package processing.utils;
2+
3+
import org.junit.jupiter.api.AfterEach;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.io.TempDir;
7+
8+
import java.io.ByteArrayInputStream;
9+
import java.io.File;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.nio.charset.StandardCharsets;
13+
import java.nio.file.Files;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertFalse;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
public class PreferencesTest {
25+
26+
@TempDir
27+
File settingsFolder;
28+
29+
@BeforeEach
30+
public void clearState() {
31+
Preferences.reset();
32+
}
33+
34+
@AfterEach
35+
public void clearOverride() {
36+
System.clearProperty("processing.app.preferences.file");
37+
Preferences.reset();
38+
}
39+
40+
41+
static InputStream stream(String content) {
42+
return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
43+
}
44+
45+
46+
/**
47+
* The bundled defaults load from the classpath, and on a first run the
48+
* table starts out identical to the defaults.
49+
*/
50+
@Test
51+
public void testInitLoadsBundledDefaults() throws IOException {
52+
Preferences.init(settingsFolder);
53+
assertNotNull(Preferences.get("editor.tabs.size"));
54+
assertEquals(Preferences.getDefault("editor.tabs.size"),
55+
Preferences.get("editor.tabs.size"));
56+
assertTrue(Preferences.isInitialized());
57+
}
58+
59+
60+
/**
61+
* A first run writes preferences.txt out to the settings folder.
62+
*/
63+
@Test
64+
public void testFirstRunCreatesPreferencesFile() throws IOException {
65+
var file = new File(settingsFolder, "preferences.txt");
66+
assertFalse(file.exists());
67+
Preferences.init(settingsFolder);
68+
assertTrue(file.exists());
69+
}
70+
71+
72+
/**
73+
* Values set and saved come back after a fresh init.
74+
*/
75+
@Test
76+
public void testSavedValuesSurviveReload() throws IOException {
77+
Preferences.init(settingsFolder);
78+
Preferences.set("editor.tabs.size", "7");
79+
Preferences.save();
80+
81+
Preferences.reset();
82+
Preferences.init(settingsFolder);
83+
assertEquals("7", Preferences.get("editor.tabs.size"));
84+
}
85+
86+
87+
/**
88+
* An existing preferences.txt overrides the bundled defaults,
89+
* but the defaults remain available via getDefault().
90+
*/
91+
@Test
92+
public void testUserPrefsOverrideDefaults() throws IOException {
93+
var file = new File(settingsFolder, "preferences.txt");
94+
Files.writeString(file.toPath(), "editor.tabs.size=9\n");
95+
96+
Preferences.init(settingsFolder);
97+
assertEquals("9", Preferences.get("editor.tabs.size"));
98+
assertEquals("2", Preferences.getDefault("editor.tabs.size"));
99+
}
100+
101+
102+
/**
103+
* The processing.app.preferences.file system property wins over the
104+
* settings folder passed in.
105+
*/
106+
@Test
107+
public void testSystemPropertyOverrideWins(@TempDir File otherFolder)
108+
throws IOException {
109+
var override = new File(otherFolder, "prefs-override.txt");
110+
Files.writeString(override.toPath(), "override.marker=yes\n");
111+
System.setProperty("processing.app.preferences.file",
112+
override.getAbsolutePath());
113+
114+
Preferences.init(settingsFolder);
115+
assertEquals("yes", Preferences.get("override.marker"));
116+
assertEquals(override.getAbsolutePath(),
117+
Preferences.getPreferencesPath());
118+
}
119+
120+
121+
/**
122+
* Keys with a suffix for this platform apply with the suffix stripped;
123+
* keys for other platforms are ignored entirely.
124+
*/
125+
@Test
126+
public void testPlatformSpecificKeys() throws IOException {
127+
Preferences.skipInit();
128+
Preferences.load(stream(
129+
"key.windows=w\nkey.macos=m\nkey.linux=l\nkey.other=o\n"));
130+
131+
var expected = switch (Platform.getName()) {
132+
case "windows" -> "w";
133+
case "macos" -> "m";
134+
case "linux" -> "l";
135+
default -> "o";
136+
};
137+
assertEquals(expected, Preferences.get("key"));
138+
assertNull(Preferences.get("key." + Platform.getName()));
139+
}
140+
141+
142+
/**
143+
* Backslashes in values are normalized to forward slashes on load.
144+
*/
145+
@Test
146+
public void testBackslashesNormalized() throws IOException {
147+
Preferences.skipInit();
148+
Preferences.load(stream("sketchbook.path.four=C:\\Users\\test\n"));
149+
assertEquals("C:/Users/test", Preferences.get("sketchbook.path.four"));
150+
}
151+
152+
153+
/**
154+
* Comment lines and blank lines don't produce entries.
155+
*/
156+
@Test
157+
public void testCommentsAndBlanksIgnored() throws IOException {
158+
Preferences.skipInit();
159+
Preferences.load(stream("# a comment\n\nkey = value\n"));
160+
assertEquals("value", Preferences.get("key"));
161+
assertNull(Preferences.get("# a comment"));
162+
}
163+
164+
165+
/**
166+
* getInteger() falls back to the default when the stored value
167+
* doesn't parse.
168+
*/
169+
@Test
170+
public void testGetIntegerFallsBackToDefault() throws IOException {
171+
Preferences.init(settingsFolder);
172+
Preferences.set("editor.tabs.size", "not-a-number");
173+
assertEquals(2, Preferences.getInteger("editor.tabs.size"));
174+
}
175+
176+
177+
/**
178+
* An unreadable preferences.txt makes init throw instead of
179+
* silently continuing.
180+
*/
181+
@Test
182+
public void testUnreadablePreferencesFileThrows() throws IOException {
183+
// a directory where the file should be cannot be opened for reading
184+
var blocked = new File(settingsFolder, "preferences.txt");
185+
assertTrue(blocked.mkdir());
186+
assertThrows(IOException.class, () -> Preferences.init(settingsFolder));
187+
}
188+
189+
190+
/**
191+
* preferences.txt is written with its keys sorted.
192+
*/
193+
@Test
194+
public void testSaveWritesSortedKeys() throws IOException {
195+
Preferences.init(settingsFolder);
196+
Preferences.set("zzz.last", "1");
197+
Preferences.set("aaa.first", "1");
198+
Preferences.save();
199+
200+
var lines = Files.readAllLines(
201+
new File(settingsFolder, "preferences.txt").toPath());
202+
// sorted by key, not by line: "a.b=…" sorts after "a=…" as a line,
203+
// but the keys "a" < "a.b" are what save() orders by
204+
var keys = new ArrayList<String>();
205+
for (var line : lines) {
206+
keys.add(line.substring(0, line.indexOf('=')));
207+
}
208+
var sorted = new ArrayList<>(keys);
209+
sorted.sort(null);
210+
assertEquals(sorted, keys);
211+
}
212+
213+
214+
/**
215+
* A 3.x sketchbook location is migrated to the 4.0 key on init,
216+
* and the migrated preference is written back out right away.
217+
*/
218+
@Test
219+
public void testSketchbookPathMigration() throws IOException {
220+
var file = new File(settingsFolder, "preferences.txt");
221+
Files.writeString(file.toPath(),
222+
"sketchbook.path.three=/old/sketchbook\n");
223+
224+
Preferences.init(settingsFolder);
225+
assertEquals("/old/sketchbook", Preferences.getSketchbookPath());
226+
assertEquals("/old/sketchbook", Preferences.getOldSketchbookPath());
227+
228+
var saved = Files.readString(file.toPath());
229+
assertTrue(saved.contains("sketchbook.path.four=/old/sketchbook"));
230+
}
231+
232+
233+
/**
234+
* An existing 4.0 sketchbook location is left alone, even when a
235+
* 3.x location is also present.
236+
*/
237+
@Test
238+
public void testExistingSketchbookPathNotOverwritten() throws IOException {
239+
var file = new File(settingsFolder, "preferences.txt");
240+
Files.writeString(file.toPath(),
241+
"sketchbook.path.three=/old\nsketchbook.path.four=/new\n");
242+
243+
Preferences.init(settingsFolder);
244+
assertEquals("/new", Preferences.getSketchbookPath());
245+
}
246+
247+
248+
/**
249+
* Listeners hear set() and unset(), but not writes of the same value.
250+
*/
251+
@Test
252+
public void testListenerSemantics() {
253+
Preferences.skipInit();
254+
List<String> events = new ArrayList<>();
255+
Preferences.ChangeListener listener =
256+
(key, value) -> events.add(key + "=" + value);
257+
Preferences.addChangeListener(listener);
258+
259+
Preferences.set("a", "1");
260+
Preferences.set("a", "1"); // unchanged, no event
261+
Preferences.set("a", "2");
262+
Preferences.unset("a");
263+
Preferences.unset("a"); // already gone, no event
264+
assertEquals(List.of("a=1", "a=2", "a=null"), events);
265+
266+
Preferences.removeChangeListener(listener);
267+
Preferences.set("a", "3");
268+
assertEquals(3, events.size());
269+
}
270+
271+
272+
/**
273+
* Bulk loading does not fire change events.
274+
*/
275+
@Test
276+
public void testLoadIsSilentForListeners() throws IOException {
277+
Preferences.skipInit();
278+
List<String> events = new ArrayList<>();
279+
Preferences.addChangeListener((key, value) -> events.add(key));
280+
281+
Preferences.load(stream("a=1\nb=2\n"));
282+
assertTrue(events.isEmpty());
283+
}
284+
285+
286+
/**
287+
* A listener that throws doesn't block the change or other listeners.
288+
*/
289+
@Test
290+
public void testListenerExceptionContained() {
291+
Preferences.skipInit();
292+
List<String> events = new ArrayList<>();
293+
Preferences.addChangeListener((key, value) -> {
294+
throw new RuntimeException("broken listener");
295+
});
296+
Preferences.addChangeListener((key, value) -> events.add(key));
297+
298+
Preferences.set("a", "1");
299+
assertEquals("1", Preferences.get("a"));
300+
assertEquals(List.of("a"), events);
301+
}
302+
}

0 commit comments

Comments
 (0)