diff --git a/app/src/main/java/com/httrack/android/OptionsActivity.java b/app/src/main/java/com/httrack/android/OptionsActivity.java index 5e42ae8e..2d90aeec 100755 --- a/app/src/main/java/com/httrack/android/OptionsActivity.java +++ b/app/src/main/java/com/httrack/android/OptionsActivity.java @@ -207,7 +207,8 @@ public static class BrowserId extends Tab { @Fields({ R.id.checkAcceptCookies, R.id.editCookiesFile, R.id.radioCheckDocumentType, R.id.checkParseJavaFiles, R.id.radioSpider, R.id.checkSitemap, R.id.editSitemapUrl, R.id.checkUpdateHacks, - R.id.checkUrlHacks, R.id.checkTolerentRequests, R.id.checkForceHttp10 }) + R.id.checkUrlHacks, R.id.editHostAlias, R.id.checkTolerentRequests, + R.id.checkForceHttp10 }) public static class Spider extends Tab { } @@ -246,8 +247,7 @@ public static class MimeDefs extends Tab { @HelpPage("guide.html#droid/opt-experts-only") @Fields({ R.id.checkUseCacheForUpdates, R.id.radioPrimaryScanRule, R.id.textTravelMode, R.id.radioTravelMode, R.id.radioGlobalTravelMode, - R.id.radioRewriteLinks, R.id.editStripQuery, R.id.editHostAlias, - R.id.checkActivateDebugging }) + R.id.radioRewriteLinks, R.id.editStripQuery, R.id.checkActivateDebugging }) public static class ExpertsOnly extends Tab { } diff --git a/app/src/main/res/layout/activity_options_expertsonly.xml b/app/src/main/res/layout/activity_options_expertsonly.xml index 990322c2..3b684967 100644 --- a/app/src/main/res/layout/activity_options_expertsonly.xml +++ b/app/src/main/res/layout/activity_options_expertsonly.xml @@ -205,19 +205,6 @@ android:inputType="textNoSuggestions" /> - - - - + + + + layouts() { - final Set files = new HashSet(); - for (final File dir : resDir().listFiles()) { - if (!dir.isDirectory() || !dir.getName().startsWith("layout")) { - continue; - } - for (final File file : dir.listFiles()) { - if (file.getName().endsWith(".xml")) { - files.add(file); - } - } - } - return files; - } - private static String option(final String id) { for (final String[] field : NUMERIC_FIELDS) { if (field[0].equals(id)) { @@ -82,7 +53,7 @@ public void numericFieldsOfferADecimalPointOnlyWhereTheEngineTakesOne() factory.setNamespaceAware(true); final DocumentBuilder builder = factory.newDocumentBuilder(); final Set seen = new HashSet(); - for (final File layout : layouts()) { + for (final File layout : TestSources.layouts()) { final Document doc = builder.parse(layout); final NodeList nodes = doc.getElementsByTagName("*"); for (int i = 0; i < nodes.getLength(); i++) { diff --git a/app/src/test/java/com/httrack/android/OptionsTabFieldsTest.java b/app/src/test/java/com/httrack/android/OptionsTabFieldsTest.java new file mode 100644 index 00000000..55b391cc --- /dev/null +++ b/app/src/test/java/com/httrack/android/OptionsTabFieldsTest.java @@ -0,0 +1,107 @@ +package com.httrack.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.Test; + +/** + * A tab's declared field must be in that tab's own layout: leaving the tab + * looks it up, and WidgetDataExchange throws on a view it cannot find. + */ +public class OptionsTabFieldsTest { + /** @ActivityId + @Fields of one tab, as declared in the source. */ + private static final Pattern TAB = Pattern.compile( + "@ActivityId\\(R\\.layout\\.(\\w+)\\)(.*?)public static class", + Pattern.DOTALL); + + private static final Pattern FIELD_ID = Pattern.compile("R\\.id\\.(\\w+)"); + + private static final Pattern LAYOUT_ID = Pattern + .compile("android:id=\"@\\+?id/(\\w+)\""); + + private static List ids(final Pattern pattern, final String text) { + final List ids = new ArrayList(); + final Matcher m = pattern.matcher(text); + while (m.find()) { + ids.add(m.group(1)); + } + return ids; + } + + /** Layout of each option tab, with the field ids that tab declares. */ + private static Map> tabs() throws IOException { + final Map> tabs = new LinkedHashMap>(); + final Matcher tab = TAB.matcher(TestSources.javaSource("OptionsActivity")); + while (tab.find()) { + final List fields = ids(FIELD_ID, tab.group(2)); + // An annotation order this pattern cannot read would yield none. + assertFalse("no field parsed for " + tab.group(1), fields.isEmpty()); + assertFalse("twice: " + tab.group(1), tabs.containsKey(tab.group(1))); + tabs.put(tab.group(1), fields); + } + assertTrue("parsed " + tabs.size() + " tabs", tabs.size() > 10); + return tabs; + } + + @Test + public void everyTabFieldLivesInThatTabsLayout() throws IOException { + for (final Map.Entry> tab : tabs().entrySet()) { + final List files = TestSources.layouts(tab.getKey()); + assertFalse("no layout named " + tab.getKey(), files.isEmpty()); + for (final File file : files) { + final Set declared = new HashSet(ids(LAYOUT_ID, + TestSources.read(file))); + for (final String field : tab.getValue()) { + assertTrue(file.getName() + " has no " + field, + declared.contains(field)); + } + } + } + } + + /* The other direction: a view the mapper knows but no tab claims is never + saved, and nothing at runtime says so. */ + @Test + public void everyMappedOptionViewIsClaimedByATab() throws IOException { + final Set mapped = new HashSet(ids(FIELD_ID, + TestSources.javaSource("OptionsMapper"))); + assertTrue("parsed " + mapped.size() + " mapped ids", mapped.size() > 80); + final Map> tabs = tabs(); + int claimed = 0; + for (final File file : TestSources.layouts()) { + if (!file.getName().startsWith("activity_options_")) { + continue; + } + final String layout = file.getName().replaceFirst("\\.xml$", ""); + for (final String id : ids(LAYOUT_ID, TestSources.read(file))) { + if (!mapped.contains(id)) { + continue; + } + final List owners = new ArrayList(); + for (final Map.Entry> tab : tabs.entrySet()) { + if (tab.getValue().contains(id)) { + owners.add(tab.getKey()); + } + } + assertEquals(id + " in " + layout + " claimed by " + owners, 1, + owners.size()); + assertEquals(id + " is claimed by " + owners.get(0), layout, + owners.get(0)); + claimed++; + } + } + assertTrue("checked " + claimed + " views", claimed > 50); + } +} diff --git a/app/src/test/java/com/httrack/android/TestSources.java b/app/src/test/java/com/httrack/android/TestSources.java new file mode 100644 index 00000000..8b49e69b --- /dev/null +++ b/app/src/test/java/com/httrack/android/TestSources.java @@ -0,0 +1,58 @@ +package com.httrack.android; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +/** Checked-in sources the tests read; they run with the app project as + * working directory. */ +final class TestSources { + private TestSources() { + } + + private static File dir(final String path) { + for (final String prefix : new String[] { "", "app/" }) { + final File dir = new File(prefix + path); + if (dir.isDirectory()) { + return dir; + } + } + throw new IllegalStateException("no " + path + " below " + + new File(".").getAbsolutePath()); + } + + /** Every layout, qualified variants such as layout-land/ included. */ + static List layouts() { + return layouts(null); + } + + /** Every variant of NAME, all of which may be inflated; every layout when + * NAME is null. */ + static List layouts(final String name) { + final List files = new ArrayList(); + for (final File res : dir("src/main/res").listFiles()) { + if (!res.isDirectory() || !res.getName().startsWith("layout")) { + continue; + } + for (final File file : res.listFiles()) { + if (name == null ? file.getName().endsWith(".xml") + : file.getName().equals(name + ".xml")) { + files.add(file); + } + } + } + return files; + } + + static String read(final File file) throws IOException { + return new String(Files.readAllBytes(file.toPath()), "UTF-8"); + } + + /** Source of the com.httrack.android class NAME. */ + static String javaSource(final String name) throws IOException { + return read(new File(dir("src/main/java"), "com/httrack/android/" + name + + ".java")); + } +}