-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from juleskers/vader-jakob
Consistency checks for dynamic strings, before adding "Frère Jacques" song.
- Loading branch information
Showing
9 changed files
with
389 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
app/src/main/java/com/nicobrailo/pianoli/song/BrotherJohn.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.nicobrailo.pianoli.song; | ||
|
||
import com.nicobrailo.pianoli.melodies.Melody; | ||
|
||
/** | ||
* The massively translated Frère Jacques ("Are you sleeping, Brother John?") | ||
* | ||
* <p> | ||
* This nursery rhyme has been translated to dozens of languages, so it should reach a very wide audience. | ||
* </p> | ||
* | ||
* <p> | ||
* Compared to the classic C-D-E-C arrangement, this version is up-shifted by three full notes, | ||
* to account for the bass-note the final chord. Without this shift, it would fall below where we have sound samples. | ||
* </p> | ||
* | ||
* | ||
* <p> | ||
* Further reading:<ul> | ||
* <li><a href="https://en.wikipedia.org/wiki/Fr%C3%A8re_Jacques">English Wikipedia: Frère Jacques</a></li> | ||
* <li><a href="https://de.wikipedia.org/wiki/Fr%C3%A8re_Jacques">German Wikipedia: Frère Jacques</a> (listing some 50 translations)</li> | ||
* </ul></p> | ||
*/ | ||
public class BrotherJohn { | ||
public static final Melody melody = Melody.fromString( | ||
"brother_john", | ||
" " + // 'useless' string so that code formatting indentation nicely lines up | ||
// Are you sleeping, 2x | ||
"F G A F " + | ||
"F G A F " + | ||
|
||
// Brother John? 2x | ||
"A A# C2 " + | ||
"A A# C2 " + | ||
|
||
// Morning bells are ringing! 2x | ||
"C2 D2 C2 A# A F " + | ||
"C2 D2 C2 A# A F " + | ||
|
||
// Please come on! 2x | ||
"F C F " + | ||
"F C F " | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
app/src/test/java/com/nicobrailo/pianoli/AssertionsExt.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.nicobrailo.pianoli; | ||
|
||
import org.junit.jupiter.api.Assertions; | ||
|
||
import java.util.Collection; | ||
|
||
public class AssertionsExt { | ||
/** | ||
* Fails (with <code>message</code>) if collection <code>haystack</code> does not contain <code>needle</code>. | ||
*/ | ||
public static <T> void assertContains(Collection<T> haystack, T needle, String message) { | ||
if (!haystack.contains(needle)) { | ||
Assertions.fail(message); | ||
} | ||
} | ||
} |
237 changes: 237 additions & 0 deletions
237
app/src/test/java/com/nicobrailo/pianoli/DynamicTranslationIdentifierTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
package com.nicobrailo.pianoli; | ||
|
||
import android.content.Context; | ||
import android.content.res.AssetManager; | ||
import com.nicobrailo.pianoli.melodies.Melody; | ||
import com.nicobrailo.pianoli.sound.SoundSet; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
|
||
import java.io.IOException; | ||
import java.lang.reflect.Field; | ||
import java.lang.reflect.Modifier; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import static com.nicobrailo.pianoli.AssertionsExt.*; | ||
|
||
/** | ||
* Consistency checks for dynamically-accessed entities: do they have translation-strings? | ||
* | ||
* <p>There are a few entities in PianOli that are derived dynamically at runtime. | ||
* The IDE/Compiler cannot us in ensuring these have internationalisation strings, or that a particular string | ||
* is never used. | ||
* These unit tests cover those holes for the following entities: | ||
* <ul> | ||
* <li>SoundSets</li> | ||
* <li>Themes</li> | ||
* <li>Melodies</li> | ||
* </ul> | ||
* </p> | ||
*/ | ||
public class DynamicTranslationIdentifierTest { | ||
/** | ||
* Ensures that all available {@link com.nicobrailo.pianoli.sound.SampledSoundSet}s are translatable. | ||
* | ||
* <p> | ||
* It does so by checking if each "soundset_FOO" folder under <code>src/main/assets/</code> has | ||
* a matching entry in <code>src/main/res/values/strings.xml</code>. | ||
* </p> | ||
* <p> | ||
* We do <em>not</em> test if the translation exists in all languages; test-failures due to (lack of) translation | ||
* progress is a distraction, and not something the developer can control. | ||
* Instead, we ensure that | ||
* </p> | ||
* | ||
* @see com.nicobrailo.pianoli.sound.SampledSoundSet | ||
* @see SoundSet#PREFIX | ||
* @see SoundSet#getAvailableSoundsets(AssetManager) | ||
* @see SettingsFragment#loadSounds() | ||
*/ | ||
@ParameterizedTest | ||
@MethodSource("getSoundSets") | ||
public void testSoundsetsHaveTranslationEntities(Path soundsetAssetFolder) { | ||
List<String> translatables = getSoundSetTranslations(); | ||
|
||
String folderName = soundsetAssetFolder.getFileName().toString(); | ||
|
||
assertContains(translatables, folderName, | ||
"Asset folder '" + soundsetAssetFolder + "' has no translation string in app/src/main/res/values/strings.xml"); | ||
} | ||
|
||
/** | ||
* Ensures our translations actually have a soundset backing them. | ||
* | ||
* <p> | ||
* Inverse of {@link #testSoundsetsHaveTranslationEntities(Path)}, useful if we rename or delete asset folders. | ||
* </p> | ||
*/ | ||
@ParameterizedTest | ||
@MethodSource("getSoundSetTranslations") | ||
public void testNoLeftoverSoundSetTranslations(String translationIdentifier) throws IOException { | ||
List<String> soundSetAssets = getSoundSets().stream() | ||
.map(Path::getFileName) | ||
.map(Path::toString) | ||
.collect(Collectors.toList()); | ||
|
||
assertContains(soundSetAssets, translationIdentifier, | ||
"Translation id '" + translationIdentifier + "' translates a soundset that doesn't exist in src/main/assets/"); | ||
} | ||
|
||
|
||
/** | ||
* Ensures that all specific {@link Theme}s are translatable. | ||
* | ||
* @see Theme | ||
* @see Theme#PREFIX | ||
* @see Theme#fromPreferences(Context) | ||
* @see Preferences#selectedTheme(Context) | ||
* @see R.xml#root_preferences | ||
*/ | ||
@ParameterizedTest | ||
@MethodSource("getThemes") | ||
public void testThemesHaveTranslationEntities(String themeName) { | ||
List<String> translatables = getThemeTranslations(); | ||
|
||
String matchingForm = Theme.PREFIX + themeName.toLowerCase(Locale.ROOT); | ||
assertContains(translatables, matchingForm, | ||
"Theme '" + themeName + "' has no translation string ('" + matchingForm + "') in app/src/main/res/values/strings.xml"); | ||
} | ||
|
||
/** | ||
* Ensures our translations actually have a Theme backing them. | ||
* | ||
* <p> | ||
* Inverse of {@link #testThemesHaveTranslationEntities(String)}, useful if we rename or delete themes. | ||
* </p> | ||
*/ | ||
@ParameterizedTest | ||
@MethodSource("getThemeTranslations") | ||
public void testNoLeftoverThemeTranslations(String translationIdentifier) { | ||
List<String> themes = getThemes(); | ||
|
||
String matchingForm = translationIdentifier | ||
.replaceFirst("^" + Theme.PREFIX, "") | ||
.toUpperCase(Locale.ROOT); | ||
assertContains(themes, matchingForm, | ||
"Translation id '" + translationIdentifier + "' translates a theme that doesn't exist in Theme.java " + | ||
"(" + themes + ")"); | ||
} | ||
|
||
|
||
/** | ||
* Ensures that all specific {@link Melody Melodies} are translatable. | ||
* | ||
* @see Melody | ||
* @see Melody#PREFIX | ||
* @see Melody#all | ||
* @see SettingsFragment#loadMelodies() | ||
*/ | ||
@ParameterizedTest | ||
@MethodSource("getMelodies") | ||
public void testMelodiesHaveTranslationEntities(String melodyId) { | ||
List<String> translatables = getMelodyTranslations(); | ||
|
||
String matchingForm = Melody.PREFIX + melodyId.toLowerCase(Locale.ROOT); | ||
assertContains(translatables, matchingForm, | ||
"Melody '" + melodyId + "' has no translation string ('" + matchingForm + "') in app/src/main/res/values/strings.xml"); | ||
} | ||
|
||
|
||
/** | ||
* Ensures our translations actually have a melody backing them. | ||
* | ||
* <p> | ||
* Inverse of {@link #testMelodiesHaveTranslationEntities(String)}, useful if we rename or delete melodies. | ||
* </p> | ||
*/ | ||
@ParameterizedTest | ||
@MethodSource("getMelodyTranslations") | ||
public void testNoLeftoverMelodyTranslations(String translationIdentifier) { | ||
List<String> melodies = getMelodies(); | ||
|
||
String matchingForm = translationIdentifier | ||
.replaceFirst("^" + Melody.PREFIX, ""); | ||
|
||
assertContains(melodies, matchingForm, | ||
"Translation id '" + translationIdentifier + "' translates a melody that doesn't exist in Melody.java " + | ||
"(" + melodies + ")"); | ||
} | ||
|
||
/** | ||
* Scans the primary translation string source for identifiers starting with <code>prefix</code> | ||
*/ | ||
private static List<String> getTranslationsByPrefix(String prefix) { | ||
// All String-resource identifiers | ||
Field[] allStrings = R.string.class.getFields(); | ||
|
||
return Arrays.stream(allStrings) | ||
.map(Field::getName) | ||
.filter(name -> name.startsWith(prefix)) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
/** | ||
* {@link MethodSource} for all soundset translation identifiers | ||
*/ | ||
public static List<String> getSoundSetTranslations() { | ||
return getTranslationsByPrefix(SoundSet.PREFIX); | ||
} | ||
|
||
/** | ||
* {@link MethodSource} for all soundset asset folders | ||
*/ | ||
@NotNull | ||
public static List<Path> getSoundSets() throws IOException { | ||
Path soundAssets = Paths.get("src/main/assets/sounds"); // app-tests run in app-folder (at least on my IDE) | ||
|
||
try (Stream<Path> pathStream = Files.list(soundAssets)) { | ||
return pathStream | ||
.filter(Files::isDirectory) // skip top-level files (specifically: "source" attribution file) | ||
.filter(path -> path.getFileName().toString().startsWith(SoundSet.PREFIX)) | ||
.collect(Collectors.toList()); | ||
} | ||
} | ||
|
||
/** | ||
* {@link MethodSource} for all theme translation identifiers | ||
*/ | ||
public static List<String> getThemeTranslations() { | ||
return getTranslationsByPrefix(Theme.PREFIX); | ||
} | ||
|
||
/** | ||
* {@link MethodSource} for all themes known to PianOli | ||
* | ||
* @see Theme | ||
*/ | ||
public static List<String> getThemes() { | ||
Field[] fields = Theme.class.getDeclaredFields(); | ||
return Arrays.stream(fields) | ||
.filter(field -> Theme.class.equals(field.getType())) | ||
.filter(field -> Modifier.isStatic(field.getModifiers())) | ||
.map(Field::getName) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
|
||
/** | ||
* {@link MethodSource} for all melody translation identifiers | ||
*/ | ||
public static List<String> getMelodyTranslations() { | ||
return getTranslationsByPrefix(Melody.PREFIX); | ||
} | ||
|
||
public static List<String> getMelodies() { | ||
return Arrays.stream(Melody.all) | ||
.map(Melody::getId) | ||
.collect(Collectors.toList()); | ||
} | ||
} |
Oops, something went wrong.