Skip to content

Commit 54c9788

Browse files
Merge pull request #11 from appdevforall/chore/phase0-closeout
chore(controller): close phase 0 — lint gate + YAML parser test
2 parents ec1660f + b40082c commit 54c9788

12 files changed

Lines changed: 5292 additions & 28 deletions

File tree

‎.github/workflows/android-sanity-check.yml‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
name: Android Sanity Check (CI)
22

33
on:
4+
# One run per PR (pull_request) and one post-merge on the integration branches.
5+
# Previously push:["**"] also ran on every feature-branch push, so an open PR
6+
# got two redundant concurrent runs. Feature branches are covered by their PR.
47
push:
5-
branches: [ "**" ]
8+
branches: [ "main", "development" ]
69
pull_request:
710
branches: [ "main", "development" ]
811

12+
# Supersede in-flight runs: when a newer commit lands on the same branch/PR, cancel
13+
# the older run so a fixup doesn't leave a zombie ~50-min lint job running.
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref }}
16+
cancel-in-progress: true
17+
918
jobs:
1019
check-syntax:
1120
name: Lint & Compile Check
@@ -35,11 +44,10 @@ jobs:
3544
- name: Run Unit Tests
3645
run: ./gradlew testDebugUnitTest --stacktrace
3746

38-
# Lint runs for visibility only and never blocks the job (continue-on-error).
39-
# Scoped to :app so the vendored termux-core/upstream lint backlog does not fail CI.
40-
# Once the :app lint backlog is triaged, set abortOnError=true and drop continue-on-error.
41-
- name: Run Android Lint (reporting, non-blocking)
42-
continue-on-error: true
47+
# Lint is a BLOCKING gate for :app. The existing backlog is grandfathered via the
48+
# committed app/lint-baseline.xml; any NEW lint error fails CI. Scoped to :app so
49+
# the vendored termux-core/upstream backlog does not block.
50+
- name: Run Android Lint (:app, blocking)
4351
run: ./gradlew :app:lintDebug
4452

4553
- name: Compile Test (Assemble Debug)

‎controller/app/build.gradle‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,15 @@ android {
102102

103103
lintOptions {
104104
checkReleaseBuilds false
105-
abortOnError false
105+
// Phase 0: lint is a hard gate for :app. The current backlog is grandfathered
106+
// via the committed baseline below, so existing issues don't block the build,
107+
// but any NEW lint error fails it. Shrink the baseline as the backlog is fixed.
108+
abortOnError true
109+
baseline file("lint-baseline.xml")
110+
// Missing translations in secondary locales (fr/hi/pt/ru) should not block
111+
// the build — en/es are primary and translations land incrementally. Still
112+
// reported as a warning so they stay visible.
113+
warning 'MissingTranslation'
106114
}
107115

108116
testOptions {

‎controller/app/lint-baseline.xml‎

Lines changed: 5133 additions & 0 deletions
Large diffs are not rendered by default.

‎controller/app/src/main/java/org/iiab/controller/DeployFragment.java‎

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
5050
import com.google.android.material.snackbar.Snackbar;
5151

52+
import org.iiab.controller.util.LocalVarsYamlParser;
5253
import org.iiab.controller.rootfs.domain.RootfsAbi;
5354
import org.iiab.controller.rootfs.domain.RootfsTier;
5455
import org.iiab.controller.rootfs.presentation.RootfsUiState;
@@ -1629,24 +1630,10 @@ private void fetchLocalVarsFromPRoot() {
16291630
}
16301631

16311632
private JSONObject parseYamlToJson(String yaml) {
1632-
JSONObject json = new JSONObject();
1633-
String[] lines = yaml.split("\n");
1634-
for (String line : lines) {
1635-
if (line.contains(":") && !line.trim().startsWith("#")) {
1636-
String[] parts = line.split(":", 2);
1637-
String key = parts[0].trim();
1638-
String val = parts[1].trim().toLowerCase();
1639-
1640-
if (key.endsWith("_install") || key.endsWith("_enabled")) {
1641-
try {
1642-
boolean isTrue = val.equals("true") || val.equals("yes") || val.equals("1");
1643-
json.put(key, isTrue);
1644-
} catch (Exception ignored) {
1645-
}
1646-
}
1647-
}
1648-
}
1649-
return json;
1633+
// Delegates to the pure, unit-tested util (extracted from this god class).
1634+
// The naive split-on-':' behavior is unchanged; replacing it with a real
1635+
// YAML parser is still tracked as tech-debt D14.
1636+
return LocalVarsYamlParser.parseToJson(yaml);
16501637
}
16511638

16521639
private void verifyInstallationState(JSONObject jsonVars) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.iiab.controller.util;
2+
3+
import org.json.JSONObject;
4+
5+
import java.util.Locale;
6+
7+
/**
8+
* Minimal reader for the slice of {@code local_vars.yml} the app cares about:
9+
* top-level {@code <module>_install} / {@code <module>_enabled} boolean flags.
10+
*
11+
* <p>Pure logic (no Android, no I/O) so it is JVM-unit-testable. It is
12+
* intentionally <strong>not</strong> a general YAML parser — it splits on the
13+
* first {@code :} and does not handle nesting, quoting, block scalars or inline
14+
* comments. That limitation is tracked as tech-debt <strong>D14</strong>; this
15+
* extraction is the first step (isolate + test) toward replacing it with a real
16+
* YAML library.
17+
*/
18+
public final class LocalVarsYamlParser {
19+
20+
private LocalVarsYamlParser() {
21+
}
22+
23+
/**
24+
* Parses {@code _install}/{@code _enabled} flags into a {@link JSONObject} of
25+
* key → boolean. A value counts as {@code true} when it is {@code true},
26+
* {@code yes} or {@code 1} (case-insensitive). Lines that are blank, comments
27+
* ({@code #}) or unrelated keys are ignored. Never returns {@code null}.
28+
*/
29+
public static JSONObject parseToJson(String yaml) {
30+
JSONObject json = new JSONObject();
31+
if (yaml == null) {
32+
return json;
33+
}
34+
for (String line : yaml.split("\n")) {
35+
if (!line.contains(":") || line.trim().startsWith("#")) {
36+
continue;
37+
}
38+
String[] parts = line.split(":", 2);
39+
String key = parts[0].trim();
40+
String val = parts[1].trim().toLowerCase(Locale.ROOT);
41+
if (key.endsWith("_install") || key.endsWith("_enabled")) {
42+
boolean isTrue = val.equals("true") || val.equals("yes") || val.equals("1");
43+
try {
44+
json.put(key, isTrue);
45+
} catch (Exception ignored) {
46+
// JSONObject.put only throws on a null key, which cannot happen here.
47+
}
48+
}
49+
}
50+
return json;
51+
}
52+
}

‎controller/app/src/main/res/values-fr/strings.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,8 @@
497497
<string name="sync_app_arch_label">Arch. de l\'App : %1$d-bit</string>
498498
<string name="sync_msg_arch_compatible">Matériel compatible. Démarrage de la connexion...</string>
499499

500+
<string name="install_btn_no_connection">Pas de connexion</string>
501+
<string name="install_msg_no_connection">Pas de connexion Internet — connectez-vous pour télécharger.</string>
502+
<string name="install_offline_estimated">Tailles estimées (hors ligne)</string>
503+
<string name="rsync_error_invalid_credentials">Identifiants de synchronisation invalides ou non sécurisés. Veuillez scanner un QR code de synchronisation IIAB valide.</string>
500504
</resources>

‎controller/app/src/main/res/values-hi/strings.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,8 @@
497497
<string name="sync_app_arch_label">App Arch: %1$d-बिट</string>
498498
<string name="sync_msg_arch_compatible">हार्डवेयर संगत है। कनेक्शन शुरू हो रहा है...</string>
499499

500+
<string name="install_btn_no_connection">कोई कनेक्शन नहीं</string>
501+
<string name="install_msg_no_connection">इंटरनेट कनेक्शन नहीं — डाउनलोड करने के लिए कनेक्ट करें।</string>
502+
<string name="install_offline_estimated">अनुमानित आकार (ऑफ़लाइन)</string>
503+
<string name="rsync_error_invalid_credentials">अमान्य या असुरक्षित सिंक क्रेडेंशियल। कृपया एक मान्य IIAB सिंक QR कोड स्कैन करें।</string>
500504
</resources>

‎controller/app/src/main/res/values-pt/strings.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,4 +499,8 @@
499499
<string name="sync_app_arch_label">Arq. do App: %1$d-bit</string>
500500
<string name="sync_msg_arch_compatible">Hardware compatível. Iniciando conexão...</string>
501501

502+
<string name="install_btn_no_connection">Sem conexão</string>
503+
<string name="install_msg_no_connection">Sem conexão à internet — conecte-se para baixar.</string>
504+
<string name="install_offline_estimated">Tamanhos estimados (offline)</string>
505+
<string name="rsync_error_invalid_credentials">Credenciais de sincronização inválidas ou inseguras. Por favor, escaneie um QR code de sincronização IIAB válido.</string>
502506
</resources>

‎controller/app/src/main/res/values-ru-rRU/strings.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,4 +496,8 @@
496496
<string name="sync_app_arch_label">Архитектура App: %1$d-бит</string>
497497
<string name="sync_msg_arch_compatible">Оборудование совместимо. Запуск подключения...</string>
498498

499+
<string name="install_btn_no_connection">Нет соединения</string>
500+
<string name="install_msg_no_connection">Нет подключения к интернету — подключитесь для загрузки.</string>
501+
<string name="install_offline_estimated">Оценочные размеры (офлайн)</string>
502+
<string name="rsync_error_invalid_credentials">Недействительные или небезопасные учётные данные синхронизации. Отсканируйте действительный QR-код синхронизации IIAB.</string>
499503
</resources>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.iiab.controller.util;
2+
3+
import static org.junit.Assert.assertFalse;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import org.json.JSONObject;
7+
import org.junit.Test;
8+
9+
/** Pure-JVM tests for the local_vars.yml flag reader. */
10+
public class LocalVarsYamlParserTest {
11+
12+
@Test
13+
public void readsInstallAndEnabledTrueVariants() {
14+
JSONObject j = LocalVarsYamlParser.parseToJson(
15+
"kiwix_install: True\n" +
16+
"kiwix_enabled: yes\n" +
17+
"kolibri_install: 1\n");
18+
assertTrue(j.optBoolean("kiwix_install"));
19+
assertTrue(j.optBoolean("kiwix_enabled"));
20+
assertTrue(j.optBoolean("kolibri_install"));
21+
}
22+
23+
@Test
24+
public void readsFalseValues() {
25+
JSONObject j = LocalVarsYamlParser.parseToJson("kiwix_install: False\nkiwix_enabled: no\n");
26+
assertFalse(j.optBoolean("kiwix_install", true));
27+
assertFalse(j.optBoolean("kiwix_enabled", true));
28+
}
29+
30+
@Test
31+
public void ignoresCommentsBlanksAndUnrelatedKeys() {
32+
JSONObject j = LocalVarsYamlParser.parseToJson(
33+
"# kiwix_install: True\n" +
34+
"\n" +
35+
"some_other_key: True\n" +
36+
"version: 3.2\n");
37+
assertFalse(j.has("kiwix_install")); // commented out
38+
assertFalse(j.has("some_other_key")); // not an _install/_enabled flag
39+
assertFalse(j.has("version"));
40+
}
41+
42+
@Test
43+
public void handlesValueContainingColon() {
44+
// split(":", 2) keeps everything after the first colon as the value.
45+
JSONObject j = LocalVarsYamlParser.parseToJson("note_enabled: true # at 10:30\n");
46+
// value is "true # at 10:30" -> not exactly "true", so it reads false.
47+
assertFalse(j.optBoolean("note_enabled", true));
48+
}
49+
50+
@Test
51+
public void handlesNullAndEmpty() {
52+
assertFalse(LocalVarsYamlParser.parseToJson(null).keys().hasNext());
53+
assertFalse(LocalVarsYamlParser.parseToJson("").keys().hasNext());
54+
}
55+
}

0 commit comments

Comments
 (0)