From 7e738a3e43354480efdb6073c1d5e7c7942d2d36 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 09:54:00 +0300 Subject: [PATCH 1/8] Add input-validation pipeline: XCUITest + Playwright gesture suite Scaffolds a minimal CN1 app under scripts/input-validation-app whose only job is to assert that physical taps, drags, and long-presses reach Component listeners end-to-end on each port. Driven by OS-level automation (XCUITest on iOS, Playwright on JavaScript) and asserted via a structured CN1IV:EVENT:* log stream -- no screenshots, no chunked Base64, no reference comparisons. Closes the test-coverage gap exposed by PR #5003: three independent input-chain regressions shipped on iOS 26 because the existing hellocodenameone suite builds every form programmatically from runTest() and never depends on a touch event actually firing. CI workflow is scaffolded but gated `if: false` pending generalisation of scripts/build-ios-app.sh (currently hard-coded to the hellocodenameone module name); manual local runs work today per the README. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/input-validation.yml | 95 ++++++++++ scripts/input-validation-app/README.adoc | 126 +++++++++++++ scripts/input-validation-app/common/pom.xml | 23 +++ .../inputvalidation/InputValidationApp.java | 27 +++ .../inputvalidation/gestures/DragStep.java | 117 ++++++++++++ .../inputvalidation/gestures/GestureStep.java | 21 +++ .../gestures/GestureSuite.java | 108 ++++++++++++ .../gestures/LongPressStep.java | 40 +++++ .../inputvalidation/gestures/TapStep.java | 35 ++++ .../drivers/playwright-driver.mjs | 140 +++++++++++++++ .../input-validation-app/drivers/run-ios.sh | 166 ++++++++++++++++++ .../input-validation-app/drivers/run-js.sh | 36 ++++ .../Sources/InputValidationUITests.swift | 71 ++++++++ .../ios-tests/project.yml | 50 ++++++ scripts/input-validation-app/ios/pom.xml | 50 ++++++ .../input-validation-app/javascript/pom.xml | 61 +++++++ scripts/input-validation-app/pom.xml | 102 +++++++++++ 17 files changed, 1268 insertions(+) create mode 100644 .github/workflows/input-validation.yml create mode 100644 scripts/input-validation-app/README.adoc create mode 100644 scripts/input-validation-app/common/pom.xml create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java create mode 100644 scripts/input-validation-app/drivers/playwright-driver.mjs create mode 100755 scripts/input-validation-app/drivers/run-ios.sh create mode 100755 scripts/input-validation-app/drivers/run-js.sh create mode 100644 scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift create mode 100644 scripts/input-validation-app/ios-tests/project.yml create mode 100644 scripts/input-validation-app/ios/pom.xml create mode 100644 scripts/input-validation-app/javascript/pom.xml create mode 100644 scripts/input-validation-app/pom.xml diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml new file mode 100644 index 0000000000..ca1483f4a1 --- /dev/null +++ b/.github/workflows/input-validation.yml @@ -0,0 +1,95 @@ +name: Input validation gesture suite + +# Fast-running multi-platform suite that asserts physical taps / drags / +# long-presses reach Component listeners end-to-end. Built to catch +# regressions in the input chain (the class of bug PR #5003 fixed -- a +# window-level UITapGestureRecognizer eating every tap was invisible to the +# existing screenshot-only tests because none of them actually depended on a +# touch event firing). Each platform job is independent; failure of one +# doesn't cancel the others. + +on: + pull_request: + paths: + - '.github/workflows/input-validation.yml' + - 'scripts/input-validation-app/**' + - 'CodenameOne/src/com/codename1/ui/Component.java' + - 'CodenameOne/src/com/codename1/ui/Form.java' + - 'CodenameOne/src/com/codename1/ui/Button.java' + - 'Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m' + - 'Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m' + - 'vm/ByteCodeTranslator/src/javascript/**' + push: + branches: [ master ] + paths: + - '.github/workflows/input-validation.yml' + - 'scripts/input-validation-app/**' + workflow_dispatch: {} + +jobs: + # iOS path is not wired into CI yet because scripts/build-ios-app.sh is + # hard-coded to the hellocodenameone module name and project file. The + # follow-up PR generalises it (CN1_APP_DIR override + dynamic xcodeproj + # name discovery) and flips this job on. Until then, the Swift target, + # XcodeGen spec, and drivers/run-ios.sh are exercised manually. + ios: + if: ${{ false }} + runs-on: macos-15 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - name: Install XcodeGen + run: brew install xcodegen + - name: Build CN1 input-validation app (iOS) + run: | + # TODO: replace with a generalised build-ios-app.sh that accepts a + # CN1_APP_DIR=scripts/input-validation-app override and emits the + # .app bundle path on stdout. + echo "iOS app build step not wired yet" >&2 + exit 1 + - name: Drive gestures via XCUITest + run: scripts/input-validation-app/drivers/run-ios.sh "$APP_BUNDLE" + env: + APP_BUNDLE: ${{ env.APP_BUNDLE }} + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: input-validation-ios + path: artifacts/input-validation-ios + if-no-files-found: warn + + javascript: + # Likewise pending a build step. The JavaScript port build is produced by + # the CN1 Maven plugin (mvn -P javascript package) but requires the JS + # toolchain pulled in by scripts/setup-workspace.sh + a static file server. + # We scaffold the job, leave it disabled, and turn it on in the follow-up + # PR that adds a publish step (or points CN1IV_URL at a known deployment). + if: ${{ false }} + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install Playwright (Chromium only) + run: | + npm install --no-save playwright + npx playwright install --with-deps chromium + - name: Build CN1 input-validation app (JavaScript) + run: | + # TODO: emit a CN1IV_URL pointing at the published bundle. + echo "JS app build step not wired yet" >&2 + exit 1 + - name: Drive gestures via Playwright + run: scripts/input-validation-app/drivers/run-js.sh "$CN1IV_URL" + env: + CN1IV_URL: ${{ env.CN1IV_URL }} + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: input-validation-js + path: artifacts/input-validation-js + if-no-files-found: warn diff --git a/scripts/input-validation-app/README.adoc b/scripts/input-validation-app/README.adoc new file mode 100644 index 0000000000..053c3c5374 --- /dev/null +++ b/scripts/input-validation-app/README.adoc @@ -0,0 +1,126 @@ += CN1 Input Validation App + +A minimal Codename One app whose only purpose is to assert that physical +input events (tap, drag, long-press) reach Component listeners end-to-end +on each port. Driven by per-platform OS-level automation (XCUITest on iOS, +Playwright on JavaScript). + +== Why a separate pipeline? + +The existing `scripts/hellocodenameone` test suite builds every form +programmatically and emits screenshots from `runTest()` without ever +depending on a touch event reaching `Component.pointerPressed`. That +gap let three independent input-chain regressions ship on iOS 26 (see +https://github.com/codenameone/CodenameOne/pull/5003): a window-level +`UITapGestureRecognizer` ate every tap, a hover recognizer cancelled +touches on the simulator, and a class-literal comparison NPE fired from +the background painter on every form show. + +This pipeline closes that gap by making input flow the only thing the +app does. There is no theme, no resource bundle, no screenshot +comparison -- the assertion is the stream of `CN1IV:EVENT:*` log lines +emitted when each gesture fires. + +== Suite + +Each step waits up to 8 seconds for its expected event, then auto-advances +on success or timeout. The driver script (XCUITest or Playwright) is +responsible for issuing the actual OS input at the expected time. + +[cols="1,3"] +|=== +| Step | What it asserts + +| `tap` +| `Button.actionPerformed` fires for a single tap. The PR #5003 + regression. + +| `drag` +| `pointerDragged` is dispatched continuously between `pointerPressed` + and `pointerReleased`, with at least 3 intermediate samples. Catches + both the iOS 26 hover-recognizer regression and the empty-pointer-array + NPE fixed in 2fef7187. + +| `longpress` +| `addLongPressListener` fires for a press-and-hold. Routes through the + same touch chain as tap, so a recognizer that cancels touches mid-press + causes this step to time out. +|=== + +== Log markers + +Everything the drivers care about is a single line on stdout: + +---- +CN1IV:SUITE:STARTED platform= w= h= +CN1IV:READY: # step is armed and waiting for input +CN1IV:EVENT::
# input arrived; advancing to next step +CN1IV:TIMEOUT: # 8s elapsed without input -- step failed +CN1IV:SUITE:FINISHED +---- + +A successful run contains one `READY:` + one `EVENT:` per step and no +`TIMEOUT:` lines. Drivers grep for these and fail non-zero on any miss. + +== Layout + +---- +scripts/input-validation-app/ +├── pom.xml # parent Maven project +├── common/ # CN1 Lifecycle + gesture screens +├── ios/ # CN1 iOS module (codename1.platform=ios) +├── javascript/ # CN1 JavaScript module (codename1.platform=javascript) +├── ios-tests/ # XCUITest target (XcodeGen-managed, no pbxproj) +└── drivers/ + ├── run-ios.sh # boot sim, install, drive gestures, assert log + ├── run-js.sh # wrap Playwright driver + └── playwright-driver.mjs +---- + +== Running locally + +=== iOS + +---- +# 1. Build the CN1 iOS .app bundle (currently goes through the regular CN1 +# iOS build path; see TODO note in .github/workflows/input-validation.yml +# -- scripts/build-ios-app.sh is being generalised to accept this app). +cd scripts/input-validation-app +mvn -P ios package +APP_BUNDLE=$(find ios/target -name '*.app' -type d | head -n 1) + +# 2. Drive the gesture suite on an iPhone 17 Pro simulator (override with +# CN1IV_DEVICE_NAME / CN1IV_DEVICE_RUNTIME). +brew install xcodegen # one-time +./drivers/run-ios.sh "$APP_BUNDLE" +---- + +=== JavaScript + +---- +# 1. Build the JS bundle and serve it (any static server will do): +cd scripts/input-validation-app +mvn -P javascript package +python3 -m http.server --directory javascript/target/ 8080 & + +# 2. Drive gestures in headless Chromium: +npm install --no-save playwright +npx playwright install --with-deps chromium +./drivers/run-js.sh http://localhost:8080 +---- + +== CI status + +The workflow at `.github/workflows/input-validation.yml` is scaffolded but +both jobs are gated by `if: false` pending build wiring -- see the TODO +comments at each `Build CN1 ...` step. They run locally today. + +== Not yet covered + +* Android (UIAutomator path + Linux emulator integration) +* JavaSE / desktop (java.awt.Robot) +* Status-bar tap-to-top +* Soft-keyboard show / character entry / dismiss +* Multi-touch / pinch + +These belong to follow-up PRs once this pipeline shape is validated. diff --git a/scripts/input-validation-app/common/pom.xml b/scripts/input-validation-app/common/pom.xml new file mode 100644 index 0000000000..8f37ff5f64 --- /dev/null +++ b/scripts/input-validation-app/common/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + com.codenameone.examples.inputvalidation + cn1-input-validation + 1.0-SNAPSHOT + + com.codenameone.examples.inputvalidation + cn1-input-validation-common + 1.0-SNAPSHOT + jar + + + + com.codenameone + codenameone-core + provided + + + diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java new file mode 100644 index 0000000000..b0801eb198 --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +package com.codenameone.inputvalidation; + +import com.codename1.system.Lifecycle; +import com.codename1.ui.CN; +import com.codename1.ui.Display; +import com.codenameone.inputvalidation.gestures.GestureSuite; + +/// Lifecycle entry point for the input-validation CN1 app. The whole app does +/// one thing: it runs `GestureSuite` once and exits. No theme, no resources, +/// no asset bundle -- by design, so a regression in input handling can never +/// hide behind a missing texture, a slow startup, or a stale screenshot +/// baseline. +public class InputValidationApp extends Lifecycle { + @Override + public void runApp() { + Runnable suite = () -> new GestureSuite().start(); + if ("HTML5".equals(Display.getInstance().getPlatformName())) { + CN.callSerially(suite); + } else { + new Thread(suite, "CN1IV-Suite").start(); + } + } +} diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java new file mode 100644 index 0000000000..c350251f89 --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/DragStep.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +package com.codenameone.inputvalidation.gestures; + +import com.codename1.ui.CN; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Font; +import com.codename1.ui.Form; +import com.codename1.ui.Graphics; +import com.codename1.ui.Label; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.layouts.BorderLayout; + +/// Validates that pointerDragged is dispatched continuously between +/// pointerPressed and pointerReleased. Covers (a) the iOS 26 hover-recognizer +/// regression in PR #5003 (which cancelled the drag mid-stream) and (b) the +/// empty-array NPE fix in 2fef7187 (`pointerDragged guard against empty pointer arrays`). +/// The detector requires at least DRAG_MIN_SAMPLES intermediate samples so a +/// recognizer that fires only the first or last point still fails. +public final class DragStep implements GestureStep { + private static final int DRAG_MIN_SAMPLES = 3; + + @Override + public String name() { + return "drag"; + } + + @Override + public void install(Container target, Callback callback) { + final DragSurface surface = new DragSurface(); + surface.setName("cn1iv-drag-target"); + Label hint = new Label("Drag horizontally across this area"); + Container col = new Container(new BorderLayout()); + col.add(BorderLayout.NORTH, hint); + col.add(BorderLayout.CENTER, surface); + target.add(BorderLayout.CENTER, col); + + final int[] samples = {0}; + final int[] firstXY = {Integer.MIN_VALUE, Integer.MIN_VALUE}; + final int[] lastXY = {Integer.MIN_VALUE, Integer.MIN_VALUE}; + final boolean[] fired = {false}; + + ActionListener dragListener = evt -> { + if (firstXY[0] == Integer.MIN_VALUE) { + firstXY[0] = evt.getX(); + firstXY[1] = evt.getY(); + } + lastXY[0] = evt.getX(); + lastXY[1] = evt.getY(); + samples[0]++; + surface.update(evt.getX(), evt.getY(), samples[0]); + }; + ActionListener releaseListener = evt -> { + if (fired[0]) { + return; + } + if (samples[0] >= DRAG_MIN_SAMPLES) { + fired[0] = true; + callback.onDetected("samples=" + samples[0] + + ",from=" + firstXY[0] + "x" + firstXY[1] + + ",to=" + lastXY[0] + "x" + lastXY[1]); + } + }; + + // Form-level listeners catch every drag sample regardless of which child + // happens to be under the finger. + Form parent = CN.getCurrentForm(); + if (parent != null) { + parent.addPointerDraggedListener(dragListener); + parent.addPointerReleasedListener(releaseListener); + } + } + + private static final class DragSurface extends Component { + private int lastX = -1; + private int lastY = -1; + private int samples; + + DragSurface() { + getAllStyles().setBgColor(0x1f2937); + getAllStyles().setBgTransparency(255); + getAllStyles().setFgColor(0xfbbf24); + getAllStyles().setMargin(16, 16, 16, 16); + getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_LARGE)); + } + + void update(int absX, int absY, int sampleCount) { + this.lastX = absX - getAbsoluteX(); + this.lastY = absY - getAbsoluteY(); + this.samples = sampleCount; + repaint(); + } + + @Override + protected com.codename1.ui.geom.Dimension calcPreferredSize() { + return new com.codename1.ui.geom.Dimension( + CN.convertToPixels(60f), + CN.convertToPixels(40f)); + } + + @Override + public void paint(Graphics g) { + g.setColor(0x1f2937); + g.fillRect(getX(), getY(), getWidth(), getHeight()); + if (this.lastX >= 0) { + g.setColor(0xfbbf24); + int r = CN.convertToPixels(3f); + g.fillArc(getX() + this.lastX - r, getY() + this.lastY - r, r * 2, r * 2, 0, 360); + } + g.setColor(0xfbbf24); + g.drawString("samples=" + this.samples, getX() + 8, getY() + 8); + } + } +} diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java new file mode 100644 index 0000000000..24c68c8256 --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureStep.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +package com.codenameone.inputvalidation.gestures; + +import com.codename1.ui.Container; + +/// One gesture under test. {@link #install} populates the supplied target +/// container with whatever UI it needs and arms its detection logic. When the +/// gesture fires, the step calls {@link Callback#onDetected(String)} exactly +/// once with optional details (e.g. sample count for a drag). +public interface GestureStep { + String name(); + + void install(Container target, Callback callback); + + interface Callback { + void onDetected(String details); + } +} diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java new file mode 100644 index 0000000000..1b58d95cef --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + */ +package com.codenameone.inputvalidation.gestures; + +import com.codename1.ui.CN; +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.Label; +import com.codename1.ui.Container; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.util.UITimer; + +/// Drives a fixed sequence of input-event tests on a single Form. Each step waits +/// for its expected gesture (tap, drag, long-press) and logs structured CN1IV:* +/// markers that the platform driver script asserts against. The state machine +/// auto-advances on either success or timeout so a broken gesture fails fast +/// without blocking the rest of the suite. +public final class GestureSuite { + private static final long DEFAULT_STEP_TIMEOUT_MS = 8000L; + private static final long SUITE_EXIT_DELAY_MS = 1500L; + + private final GestureStep[] steps; + private final Form form; + private final Label statusLabel; + private final Container targetArea; + private int index = -1; + private UITimer activeTimeout; + + public GestureSuite() { + this.steps = new GestureStep[] { + new TapStep(), + new DragStep(), + new LongPressStep() + }; + this.form = new Form("Input Validation", new BorderLayout()); + this.statusLabel = new Label("Initializing"); + this.statusLabel.setName("cn1iv-status"); + Container top = new Container(BoxLayout.y()); + top.add(this.statusLabel); + this.targetArea = new Container(new BorderLayout()); + this.targetArea.setName("cn1iv-target"); + this.form.add(BorderLayout.NORTH, top); + this.form.add(BorderLayout.CENTER, this.targetArea); + } + + public void start() { + log("CN1IV:SUITE:STARTED platform=" + CN.getPlatformName() + + " w=" + Display.getInstance().getDisplayWidth() + + " h=" + Display.getInstance().getDisplayHeight()); + this.form.show(); + CN.callSerially(this::advance); + } + + private void advance() { + cancelTimeout(); + this.index++; + if (this.index >= this.steps.length) { + finishSuite(); + return; + } + final GestureStep step = this.steps[this.index]; + this.statusLabel.setText("Step " + (this.index + 1) + "/" + this.steps.length + + ": " + step.name()); + this.targetArea.removeAll(); + step.install(this.targetArea, new GestureStep.Callback() { + @Override + public void onDetected(String details) { + log("CN1IV:EVENT:" + step.name() + (details == null ? "" : ":" + details)); + CN.callSerially(GestureSuite.this::advance); + } + }); + this.targetArea.revalidate(); + log("CN1IV:READY:" + step.name()); + this.activeTimeout = UITimer.timer((int) DEFAULT_STEP_TIMEOUT_MS, false, this.form, () -> { + log("CN1IV:TIMEOUT:" + step.name()); + advance(); + }); + } + + private void cancelTimeout() { + if (this.activeTimeout != null) { + this.activeTimeout.cancel(); + this.activeTimeout = null; + } + } + + private void finishSuite() { + log("CN1IV:SUITE:FINISHED"); + UITimer.timer((int) SUITE_EXIT_DELAY_MS, false, this.form, () -> { + try { + Display.getInstance().exitApplication(); + } catch (Throwable ignored) { + } + }); + } + + private static void log(String line) { + System.out.println(line); + } +} diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java new file mode 100644 index 0000000000..0ce29c6cfa --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/LongPressStep.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +package com.codenameone.inputvalidation.gestures; + +import com.codename1.ui.Button; +import com.codename1.ui.Container; +import com.codename1.ui.Font; +import com.codename1.ui.layouts.BorderLayout; + +/// Validates that addLongPressListener fires for a press-and-hold gesture. +/// Long-press uses the same touch chain as tap on iOS; if a window-level +/// recognizer cancels touches mid-press (the PR #5003 path), the long-press +/// listener never fires and this step times out. +public final class LongPressStep implements GestureStep { + @Override + public String name() { + return "longpress"; + } + + @Override + public void install(Container target, Callback callback) { + Button btn = new Button("Long-press me"); + btn.setName("cn1iv-longpress-target"); + btn.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE)); + btn.getAllStyles().setPadding(48, 48, 48, 48); + btn.getAllStyles().setMargin(48, 48, 48, 48); + btn.getAllStyles().setBgColor(0x16a34a); + btn.getAllStyles().setBgTransparency(255); + btn.getAllStyles().setFgColor(0xffffff); + final long[] pressedAt = {0L}; + btn.addPointerPressedListener(evt -> pressedAt[0] = System.currentTimeMillis()); + btn.addLongPressListener(evt -> { + long elapsed = pressedAt[0] == 0L ? -1 : (System.currentTimeMillis() - pressedAt[0]); + callback.onDetected("durMs=" + elapsed); + }); + target.add(BorderLayout.CENTER, btn); + } +} diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java new file mode 100644 index 0000000000..cca4892ade --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/TapStep.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +package com.codenameone.inputvalidation.gestures; + +import com.codename1.ui.Button; +import com.codename1.ui.Container; +import com.codename1.ui.Font; +import com.codename1.ui.layouts.BorderLayout; + +/// Validates that a single tap dispatches Button.actionPerformed end-to-end. +/// This is the regression PR #5003 fixed -- a window-level UITapGestureRecognizer +/// on iOS 26 was eating the touch before CN1TapGestureRecognizer saw it, so +/// every button tap silently did nothing. +public final class TapStep implements GestureStep { + @Override + public String name() { + return "tap"; + } + + @Override + public void install(Container target, Callback callback) { + Button btn = new Button("Tap me"); + btn.setName("cn1iv-tap-target"); + btn.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE)); + btn.getAllStyles().setPadding(48, 48, 48, 48); + btn.getAllStyles().setMargin(48, 48, 48, 48); + btn.getAllStyles().setBgColor(0x2563eb); + btn.getAllStyles().setBgTransparency(255); + btn.getAllStyles().setFgColor(0xffffff); + btn.addActionListener(evt -> callback.onDetected("x=" + evt.getX() + ",y=" + evt.getY())); + target.add(BorderLayout.CENTER, btn); + } +} diff --git a/scripts/input-validation-app/drivers/playwright-driver.mjs b/scripts/input-validation-app/drivers/playwright-driver.mjs new file mode 100644 index 0000000000..6b4df91969 --- /dev/null +++ b/scripts/input-validation-app/drivers/playwright-driver.mjs @@ -0,0 +1,140 @@ +// Drive the CN1 input-validation app (JavaScript port) through tap / drag / +// long-press in headless Chromium and assert the expected CN1IV:EVENT lines +// appear on the browser console. Mirrors the iOS XCUITest driver -- same +// suite, same log markers, same pass/fail contract. + +import fs from 'node:fs'; +import path from 'node:path'; + +let chromium; +try { + ({ chromium } = await import('playwright')); +} catch (e1) { + try { + ({ chromium } = await import('@playwright/test')); + } catch (e2) { + console.error('Unable to load Playwright. Install "playwright" or "@playwright/test".'); + process.exit(2); + } +} + +const url = process.env.CN1IV_URL; +if (!url) { + console.error('CN1IV_URL env var is required (URL of the deployed JS build).'); + process.exit(2); +} + +const artifactsDir = process.env.CN1IV_ARTIFACTS_DIR + || path.resolve('artifacts/input-validation-js'); +fs.mkdirSync(artifactsDir, { recursive: true }); +const logPath = path.join(artifactsDir, 'browser.log'); +const logStream = fs.createWriteStream(logPath, { flags: 'w' }); + +const REQUIRED_EVENTS = [ + 'CN1IV:READY:tap', + 'CN1IV:EVENT:tap', + 'CN1IV:READY:drag', + 'CN1IV:EVENT:drag', + 'CN1IV:READY:longpress', + 'CN1IV:EVENT:longpress', + 'CN1IV:SUITE:FINISHED', +]; + +// We capture every console line, but only the lines containing CN1IV are +// considered for assertion. The page also dumps stack traces on errors, so +// we tee everything to disk for post-mortem. +const seen = new Set(); +const timeouts = []; +function record(line) { + logStream.write(line + '\n'); + if (line.indexOf('CN1IV:TIMEOUT:') >= 0) { + timeouts.push(line); + } + for (const m of REQUIRED_EVENTS) { + if (!seen.has(m) && line.indexOf(m) >= 0) { + seen.add(m); + } + } +} + +const browser = await chromium.launch({ headless: true }); +const viewport = { width: 393, height: 852 }; +try { + const page = await browser.newPage({ viewport, deviceScaleFactor: 2 }); + page.on('console', msg => record(`console:${msg.type()}:${msg.text()}`)); + page.on('pageerror', err => record(`pageerror:${String(err)}`)); + + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 }); + + // Wait for the CN1 EDT to finish first paint before driving inputs. CN1's + // JS port emits CN1IV:READY:tap from GestureSuite.advance() right after + // Form.show(); we poll for it with a generous ceiling so flaky CI machines + // still pass. + const readyDeadline = Date.now() + 30_000; + while (Date.now() < readyDeadline && !seen.has('CN1IV:READY:tap')) { + await page.waitForTimeout(250); + } + if (!seen.has('CN1IV:READY:tap')) { + console.error('Timed out waiting for CN1IV:READY:tap from app.'); + process.exit(1); + } + + // Tap: click the centre of the form. + await page.mouse.click(viewport.width / 2, viewport.height / 2); + await waitFor('CN1IV:EVENT:tap', 5_000); + + // Drag: sweep horizontally so we exceed the 3-sample floor in DragStep. + // page.mouse.move with steps=N emits N mousemove events -> CN1 sees N + // pointerDragged calls between the down/up pair. + await waitFor('CN1IV:READY:drag', 5_000); + await page.mouse.move(viewport.width * 0.2, viewport.height * 0.55); + await page.mouse.down(); + await page.mouse.move(viewport.width * 0.8, viewport.height * 0.55, { steps: 12 }); + await page.mouse.up(); + await waitFor('CN1IV:EVENT:drag', 5_000); + + // Long-press: mousedown, hold, mouseup. CN1's long-press threshold is + // ~1s by default; 1500ms gives us comfortable headroom. + await waitFor('CN1IV:READY:longpress', 5_000); + await page.mouse.move(viewport.width / 2, viewport.height / 2); + await page.mouse.down(); + await page.waitForTimeout(1500); + await page.mouse.up(); + await waitFor('CN1IV:EVENT:longpress', 5_000); + + await waitFor('CN1IV:SUITE:FINISHED', 5_000); + + async function waitFor(marker, ms) { + const deadline = Date.now() + ms; + while (Date.now() < deadline) { + if (seen.has(marker)) return; + await page.waitForTimeout(150); + } + // Don't throw here -- continue so we get the full list of misses in the + // final report. + } +} finally { + await browser.close(); + logStream.end(); +} + +const missing = REQUIRED_EVENTS.filter(m => !seen.has(m)); +const log = (line) => process.stdout.write(`[playwright-driver] ${line}\n`); +let failed = false; +for (const m of REQUIRED_EVENTS) { + if (seen.has(m)) { + log(`OK ${m}`); + } else { + log(`MISS ${m}`); + failed = true; + } +} +if (timeouts.length) { + for (const t of timeouts) log(`TIMEOUT ${t}`); + failed = true; +} +if (failed) { + log(`Input-validation suite FAILED -- see ${logPath}`); + process.exit(1); +} +log('Input-validation suite PASSED'); diff --git a/scripts/input-validation-app/drivers/run-ios.sh b/scripts/input-validation-app/drivers/run-ios.sh new file mode 100755 index 0000000000..804f0c1b13 --- /dev/null +++ b/scripts/input-validation-app/drivers/run-ios.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +# Drive the CN1 input-validation app through tap / drag / long-press on an +# iOS simulator and assert the expected CN1IV:EVENT log lines appear. +# +# Usage: +# run-ios.sh +# +# The .app bundle is produced by `mvn -P ios package` against the parent POM +# in scripts/input-validation-app and the cn1-builder build server (or local +# iOS build chain). This script is intentionally lean -- no screenshot +# decoding, no chunked Base64, no comparison report. The only thing it cares +# about is whether the OS-level taps reached Component listeners. +set -euo pipefail + +iv_log() { echo "[run-ios] $1"; } + +if [ $# -lt 1 ]; then + iv_log "Usage: $0 " >&2 + exit 2 +fi + +APP_BUNDLE="$1" +if [ ! -d "$APP_BUNDLE" ]; then + iv_log "App bundle not found: $APP_BUNDLE" >&2 + exit 3 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TESTS_DIR="$APP_DIR/ios-tests" +ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$APP_DIR}/artifacts/input-validation-ios}" +mkdir -p "$ARTIFACTS_DIR" +LOG_FILE="$ARTIFACTS_DIR/device.log" +XCODEBUILD_LOG="$ARTIFACTS_DIR/xcodebuild-test.log" + +if ! command -v xcrun >/dev/null 2>&1; then iv_log "xcrun not on PATH" >&2; exit 3; fi +if ! command -v xcodebuild >/dev/null 2>&1; then iv_log "xcodebuild not on PATH" >&2; exit 3; fi +if ! command -v xcodegen >/dev/null 2>&1; then + iv_log "xcodegen not on PATH. Install with: brew install xcodegen" >&2 + exit 3 +fi + +# Read the app's actual bundle identifier from its Info.plist so we don't +# guess wrong if the CN1 generator changes its default. +BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_BUNDLE/Info.plist" 2>/dev/null || true)" +if [ -z "$BUNDLE_ID" ]; then + iv_log "Could not read CFBundleIdentifier from $APP_BUNDLE/Info.plist" >&2 + exit 3 +fi +iv_log "Bundle id: $BUNDLE_ID" + +DEVICE_NAME="${CN1IV_DEVICE_NAME:-iPhone 17 Pro}" +DEVICE_RUNTIME="${CN1IV_DEVICE_RUNTIME:-}" + +iv_log "Locating simulator: $DEVICE_NAME" +SIM_UDID="" +while IFS=$'\t' read -r udid name state runtime; do + if [ "$name" = "$DEVICE_NAME" ] && { [ -z "$DEVICE_RUNTIME" ] || [ "$runtime" = "$DEVICE_RUNTIME" ]; }; then + SIM_UDID="$udid" + break + fi +done < <(xcrun simctl list devices available -j \ + | python3 -c ' +import json, sys +data = json.load(sys.stdin) +for runtime, devs in data.get("devices", {}).items(): + for d in devs: + if d.get("isAvailable"): + print(f"{d[\"udid\"]}\t{d[\"name\"]}\t{d[\"state\"]}\t{runtime}") +') + +if [ -z "$SIM_UDID" ]; then + iv_log "No simulator matched name=$DEVICE_NAME runtime=$DEVICE_RUNTIME" >&2 + xcrun simctl list devices available >&2 || true + exit 3 +fi +iv_log "Using simulator $SIM_UDID" + +# Boot the simulator if needed. `bootstatus -b` blocks until SpringBoard is up. +xcrun simctl boot "$SIM_UDID" >/dev/null 2>&1 || true +xcrun simctl bootstatus "$SIM_UDID" -b + +# Install the app fresh -- uninstall first so a stale bundle doesn't shadow the +# new one when the bundle identifier collides. +xcrun simctl uninstall "$SIM_UDID" "$BUNDLE_ID" >/dev/null 2>&1 || true +iv_log "Installing $APP_BUNDLE" +xcrun simctl install "$SIM_UDID" "$APP_BUNDLE" + +# Start streaming os_log lines that came from the CN1 process (printf -> NSLog +# on the iOS port routes through unified logging). Capture in the background; +# we'll wait on the file after the XCUITest run. +iv_log "Starting log stream -> $LOG_FILE" +: > "$LOG_FILE" +xcrun simctl spawn "$SIM_UDID" log stream \ + --style compact --level debug \ + --predicate '(processImagePath CONTAINS[c] "'"$BUNDLE_ID"'") OR (eventMessage CONTAINS "CN1IV:")' \ + > "$LOG_FILE" 2>&1 & +LOG_PID=$! +cleanup() { + kill "$LOG_PID" 2>/dev/null || true + wait "$LOG_PID" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +# Generate the XCUITest Xcode project on demand. We don't check in pbxproj. +iv_log "Generating XCUITest project via xcodegen" +( cd "$TESTS_DIR" && xcodegen generate >> "$XCODEBUILD_LOG" 2>&1 ) + +# Run the XCUITest suite. CN1IV_BUNDLE_ID tells the Swift code which app to +# attach to; CN1IV_STEP_DELAY_SEC lets us slow the inter-gesture wait on +# heavily loaded CI runners. +iv_log "Running XCUITest" +set +e +xcodebuild test \ + -project "$TESTS_DIR/CN1InputValidationUITests.xcodeproj" \ + -scheme CN1InputValidationUITests \ + -destination "platform=iOS Simulator,id=$SIM_UDID" \ + CN1IV_BUNDLE_ID="$BUNDLE_ID" \ + CODE_SIGNING_ALLOWED=NO \ + | tee -a "$XCODEBUILD_LOG" +XCB_RC=${PIPESTATUS[0]} +set -e +iv_log "xcodebuild test exit=$XCB_RC" + +# Give the log stream a beat to flush the final CN1IV:SUITE:FINISHED line. +sleep 2 +cleanup +trap - EXIT INT TERM + +# Assertion: each expected event must appear at least once in the log. +REQUIRED_EVENTS=( + "CN1IV:READY:tap" + "CN1IV:EVENT:tap" + "CN1IV:READY:drag" + "CN1IV:EVENT:drag" + "CN1IV:READY:longpress" + "CN1IV:EVENT:longpress" + "CN1IV:SUITE:FINISHED" +) +FAILED=0 +for needle in "${REQUIRED_EVENTS[@]}"; do + if grep -q "$needle" "$LOG_FILE"; then + iv_log "OK $needle" + else + iv_log "MISS $needle" + FAILED=1 + fi +done + +if grep -qE 'CN1IV:TIMEOUT:' "$LOG_FILE"; then + iv_log "Gesture timeouts detected in device log:" + grep -E 'CN1IV:TIMEOUT:' "$LOG_FILE" | sed 's/^/ /' + FAILED=1 +fi + +if [ "$XCB_RC" -ne 0 ]; then + iv_log "xcodebuild test failed (rc=$XCB_RC) -- see $XCODEBUILD_LOG" + FAILED=1 +fi + +if [ "$FAILED" -ne 0 ]; then + iv_log "Input-validation suite FAILED -- see $LOG_FILE" + exit 1 +fi + +iv_log "Input-validation suite PASSED" diff --git a/scripts/input-validation-app/drivers/run-js.sh b/scripts/input-validation-app/drivers/run-js.sh new file mode 100755 index 0000000000..b83e909b89 --- /dev/null +++ b/scripts/input-validation-app/drivers/run-js.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Drive the CN1 input-validation app in headless Chromium against a URL +# serving the built JavaScript port. Thin wrapper around playwright-driver.mjs. +# +# Usage: +# run-js.sh +# +# Assumes Playwright (with Chromium) is installed -- the parent Maven build +# leaves a node_modules/ alongside the JS port output that has it; CI installs +# globally. +set -euo pipefail + +iv_log() { echo "[run-js] $1"; } + +if [ $# -lt 1 ]; then + iv_log "Usage: $0 " >&2 + exit 2 +fi + +URL="$1" + +if ! command -v node >/dev/null 2>&1; then + iv_log "node not on PATH" >&2 + exit 3 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$APP_DIR}/artifacts/input-validation-js}" +mkdir -p "$ARTIFACTS_DIR" + +export CN1IV_URL="$URL" +export CN1IV_ARTIFACTS_DIR="$ARTIFACTS_DIR" + +iv_log "Driving $URL" +node "$SCRIPT_DIR/playwright-driver.mjs" diff --git a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift new file mode 100644 index 0000000000..cb1ab2c712 --- /dev/null +++ b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift @@ -0,0 +1,71 @@ +// Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. +// DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +// +// XCUITest target that drives the CN1 input-validation app through tap, +// drag, and long-press gestures on the iOS simulator. We rely on coordinate +// taps rather than accessibility queries because the CN1 iOS port does not +// surface child Components as XCUIElements -- the whole CN1 form renders into +// one GL/Metal-backed view from XCUITest's perspective. The driver shell +// script asserts the CN1IV:EVENT:* lines appear in the os_log stream; this +// file only sequences the physical inputs. + +import XCTest + +final class InputValidationUITests: XCTestCase { + // Bundle identifier of the CN1-built iOS app under test. Set from the + // CN1IV_BUNDLE_ID env var so the driver script can target whatever value + // the CN1 Maven plugin generated, without us hard-coding it. + private var bundleIdentifier: String { + return ProcessInfo.processInfo.environment["CN1IV_BUNDLE_ID"] + ?? "com.codenameone.examples.inputvalidation" + } + + private var stepDelaySeconds: TimeInterval { + if let raw = ProcessInfo.processInfo.environment["CN1IV_STEP_DELAY_SEC"], + let v = Double(raw) { + return v + } + return 3.0 + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testGestureSuite() throws { + let app = XCUIApplication(bundleIdentifier: bundleIdentifier) + app.launch() + // Wait for the form to render before driving inputs. The CN1 EDT needs + // a moment after process launch to mount the GLViewController, run the + // first paint, and start dispatching pointer events. Without this delay + // taps fire into the splash screen. + Thread.sleep(forTimeInterval: 2.5) + + try driveTap(app: app) + Thread.sleep(forTimeInterval: stepDelaySeconds) + + try driveDrag(app: app) + Thread.sleep(forTimeInterval: stepDelaySeconds) + + try driveLongPress(app: app) + Thread.sleep(forTimeInterval: stepDelaySeconds) + } + + private func driveTap(app: XCUIApplication) throws { + let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) + center.tap() + } + + private func driveDrag(app: XCUIApplication) throws { + // Sweep horizontally across the middle band so the CN1 drag detector + // collects enough pointerDragged samples to exceed its 3-sample floor. + let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.55)) + let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.55)) + start.press(forDuration: 0.05, thenDragTo: end) + } + + private func driveLongPress(app: XCUIApplication) throws { + let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) + center.press(forDuration: 1.5) + } +} diff --git a/scripts/input-validation-app/ios-tests/project.yml b/scripts/input-validation-app/ios-tests/project.yml new file mode 100644 index 0000000000..c64d757b1f --- /dev/null +++ b/scripts/input-validation-app/ios-tests/project.yml @@ -0,0 +1,50 @@ +# XcodeGen project spec for the CN1 input-validation XCUITest target. +# +# We deliberately do not check in a .xcodeproj -- pbxproj diffs are unreadable +# and break on every Xcode update. Instead the driver script invokes +# `xcodegen generate` against this file to materialise the project on demand. +# +# The XCUITest target launches the already-installed CN1 app by bundle id +# (see InputValidationUITests.swift) rather than building it here, so we have +# no host-app dependency and don't need provisioning profiles. The test runs +# under XCTest's `application-only` UI-testing mode, which works on simulators +# out of the box. + +name: CN1InputValidationUITests +options: + deploymentTarget: + iOS: "16.0" + minimumXcodeGenVersion: "2.38.0" + bundleIdPrefix: com.codenameone.cn1iv + createIntermediateGroups: true + +settings: + base: + SWIFT_VERSION: "5.0" + CODE_SIGNING_ALLOWED: NO + +targets: + CN1InputValidationUITests: + type: bundle.ui-testing + platform: iOS + sources: + - Sources + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.codenameone.cn1iv.uitests + TEST_TARGET_NAME: "" + USES_XCTRUNNER: YES + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: YES + CODE_SIGNING_ALLOWED: NO + CODE_SIGN_IDENTITY: "" + +schemes: + CN1InputValidationUITests: + build: + targets: + CN1InputValidationUITests: [test] + test: + targets: + - CN1InputValidationUITests + gatherCoverageData: false + parallelizable: false diff --git a/scripts/input-validation-app/ios/pom.xml b/scripts/input-validation-app/ios/pom.xml new file mode 100644 index 0000000000..fa3e8b18fc --- /dev/null +++ b/scripts/input-validation-app/ios/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + com.codenameone.examples.inputvalidation + cn1-input-validation + 1.0-SNAPSHOT + + com.codenameone.examples.inputvalidation + cn1-input-validation-ios + 1.0-SNAPSHOT + + + UTF-8 + 17 + 17 + ios + ios + ios-device + + + + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + build-ios + package + + build + + + + + + + + + + ${project.groupId} + ${cn1app.name}-common + ${project.version} + + + diff --git a/scripts/input-validation-app/javascript/pom.xml b/scripts/input-validation-app/javascript/pom.xml new file mode 100644 index 0000000000..467b5e0b1f --- /dev/null +++ b/scripts/input-validation-app/javascript/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + com.codenameone.examples.inputvalidation + cn1-input-validation + 1.0-SNAPSHOT + + com.codenameone.examples.inputvalidation + cn1-input-validation-javascript + 1.0-SNAPSHOT + + + UTF-8 + 17 + 17 + javascript + javascript + javascript + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.1 + + + default-jar + none + + + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + build-javascript + package + + build + + + + + + + + + + ${project.groupId} + ${cn1app.name}-common + ${project.version} + + + diff --git a/scripts/input-validation-app/pom.xml b/scripts/input-validation-app/pom.xml new file mode 100644 index 0000000000..112ec8361a --- /dev/null +++ b/scripts/input-validation-app/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + com.codenameone.examples.inputvalidation + cn1-input-validation + 1.0-SNAPSHOT + pom + cn1-input-validation + + Minimal CN1 app whose only job is to validate that input events + (tap, drag, long-press) reach Component listeners end-to-end on each + port. Driven by per-platform OS-level automation (XCUITest, Playwright). + + + + common + + + + 8.0-SNAPSHOT + 8.0-SNAPSHOT + UTF-8 + 17 + 3.8.0 + 17 + 17 + cn1-input-validation + + + + + + com.codenameone + java-runtime + ${cn1.version} + + + com.codenameone + codenameone-core + ${cn1.version} + + + com.codenameone + codenameone-javase + ${cn1.version} + + + com.codenameone + codenameone-buildclient + ${cn1.version} + system + ${user.home}/.codenameone/CodeNameOneBuildClient.jar + + + + + + + + + com.codenameone + codenameone-maven-plugin + ${cn1.plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + + + + + ios + + + codename1.platform + ios + + + + ios + + + + javascript + + + codename1.platform + javascript + + + + javascript + + + + From db81697c9e6b77601ffafaace979470b3ec13554 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 12:30:26 +0300 Subject: [PATCH 2/8] Remove unused `missing` variable in Playwright driver The misses are already reported by the for-loop over REQUIRED_EVENTS below, so the precomputed array was dead code. Flagged by github-code-quality on PR #5005. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/input-validation-app/drivers/playwright-driver.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/input-validation-app/drivers/playwright-driver.mjs b/scripts/input-validation-app/drivers/playwright-driver.mjs index 6b4df91969..d43e42573d 100644 --- a/scripts/input-validation-app/drivers/playwright-driver.mjs +++ b/scripts/input-validation-app/drivers/playwright-driver.mjs @@ -118,7 +118,6 @@ try { logStream.end(); } -const missing = REQUIRED_EVENTS.filter(m => !seen.has(m)); const log = (line) => process.stdout.write(`[playwright-driver] ${line}\n`); let failed = false; for (const m of REQUIRED_EVENTS) { From 4f46f8e14fc4a44a4a6beb34f2590e1840f8551d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 12:39:39 +0300 Subject: [PATCH 3/8] Wire input-validation iOS + JS jobs into actual CI runs Generalise the two build scripts via CN1_APP_DIR / CN1_APP_MAIN_NAME / CN1_APP_PACKAGE_NAME env overrides -- defaults stay pointed at hellocodenameone so the existing scripts-ios.yml / scripts-javascript.yml pipelines remain unchanged: * scripts/build-ios-app.sh: read APP_MAIN_NAME from codenameone_settings.properties; templated workspace, scheme and Xcode project names off it so any CN1 app can be built. * scripts/build-javascript-port-hellocodenameone.sh: read packageName/mainName from settings, parameterise the generated JavaScript launcher and the ByteCodeTranslator invocation. Add the input-validation app's CN1 project metadata so the CN1 plugin knows what to name everything: codenameone_settings.properties (mainName= InputValidationApp, packageName=com.codenameone.inputvalidation), a stub icon.png copied from hellocodenameone, and the Maven wrapper alongside. Rewrite .github/workflows/input-validation.yml so both jobs actually run rather than being gated by `if: false`: * `build-port` (reusable from scripts-ios.yml) builds + caches the CN1 iOS port. * `ios` consumes that cache, runs build-ios-app.sh with CN1_APP_DIR= scripts/input-validation-app, builds the .app via xcodebuild for the simulator, and hands it to drivers/run-ios.sh which drives gestures via XCUITest and asserts CN1IV:* lines in the os_log stream. * `javascript` builds the ParparVM browser bundle, serves it from python3 -m http.server, and drives Chromium via drivers/run-js.sh. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/input-validation.yml | 310 +++++++++++++++-- scripts/build-ios-app.sh | 41 ++- .../build-javascript-port-hellocodenameone.sh | 55 ++- scripts/input-validation-app/.mvn/jvm.config | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 117 +++++++ .../.mvn/wrapper/maven-wrapper.properties | 2 + scripts/input-validation-app/README.adoc | 15 +- .../common/codenameone_settings.properties | 27 ++ scripts/input-validation-app/common/icon.png | Bin 0 -> 123269 bytes scripts/input-validation-app/mvnw | 327 ++++++++++++++++++ 10 files changed, 824 insertions(+), 70 deletions(-) create mode 100644 scripts/input-validation-app/.mvn/jvm.config create mode 100644 scripts/input-validation-app/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 scripts/input-validation-app/.mvn/wrapper/maven-wrapper.properties create mode 100644 scripts/input-validation-app/common/codenameone_settings.properties create mode 100644 scripts/input-validation-app/common/icon.png create mode 100755 scripts/input-validation-app/mvnw diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml index ca1483f4a1..055fea090c 100644 --- a/.github/workflows/input-validation.yml +++ b/.github/workflows/input-validation.yml @@ -13,83 +13,325 @@ on: paths: - '.github/workflows/input-validation.yml' - 'scripts/input-validation-app/**' + - 'scripts/build-ios-app.sh' + - 'scripts/build-javascript-port-hellocodenameone.sh' - 'CodenameOne/src/com/codename1/ui/Component.java' - 'CodenameOne/src/com/codename1/ui/Form.java' - 'CodenameOne/src/com/codename1/ui/Button.java' - 'Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m' - 'Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m' + - 'Ports/JavaScriptPort/**' - 'vm/ByteCodeTranslator/src/javascript/**' push: branches: [ master ] paths: - '.github/workflows/input-validation.yml' - 'scripts/input-validation-app/**' + - 'scripts/build-ios-app.sh' + - 'scripts/build-javascript-port-hellocodenameone.sh' workflow_dispatch: {} jobs: - # iOS path is not wired into CI yet because scripts/build-ios-app.sh is - # hard-coded to the hellocodenameone module name and project file. The - # follow-up PR generalises it (CN1_APP_DIR override + dynamic xcodeproj - # name discovery) and flips this job on. Until then, the Swift target, - # XcodeGen spec, and drivers/run-ios.sh are exercised manually. + build-port: + # Reused by `ios` so the iOS port .jar / native sources are available in + # the cache the build-ios-app step relies on. Same reusable workflow that + # scripts-ios.yml depends on -- piggybacking on its cache key. + uses: ./.github/workflows/_build-ios-port.yml + ios: - if: ${{ false }} + needs: build-port + permissions: + contents: read runs-on: macos-15 - timeout-minutes: 25 + timeout-minutes: 35 + concurrency: + group: mac-ci-${{ github.workflow }}-ios-${{ github.ref_name }} + cancel-in-progress: true + env: + GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + CN1_APP_DIR: scripts/input-validation-app steps: - uses: actions/checkout@v4 + + - name: Cache CocoaPods and user gems + uses: actions/cache@v4 + with: + path: | + ~/.gem + ~/Library/Caches/CocoaPods + ~/.cocoapods/repos + key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }} + restore-keys: | + ${{ runner.os }}-pods-v1- + + - name: Ensure CocoaPods tooling + run: | + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + set -euo pipefail + GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')" + export PATH="$GEM_USER_DIR/bin:$PATH" + if ! command -v pod >/dev/null 2>&1; then + gem install cocoapods xcodeproj --no-document --user-install + fi + pod --version + + - name: Compute setup-workspace hash + id: setup_hash + run: echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Compute CN1 source hash + id: src_hash + run: | + set -euo pipefail + SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \ + -type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \ + | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}') + POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \ + | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}') + SCRIPT_HASH=$(shasum -a 256 \ + scripts/setup-workspace.sh \ + scripts/build-ios-port.sh \ + scripts/build-native-themes.sh \ + .github/workflows/_build-ios-port.yml \ + | shasum -a 256 | awk '{print $1}') + echo "hash=${SRC_HASH:0:16}-${POM_HASH:0:16}-${SCRIPT_HASH:0:16}" >> "$GITHUB_OUTPUT" + + - name: Set TMPDIR + run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + + - name: Cache codenameone-tools + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/codenameone-tools + key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + ${{ runner.os }}-cn1-tools- + + - name: Cache Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2- + + - name: Restore cn1-binaries cache + uses: actions/cache@v4 + with: + path: ../cn1-binaries + key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + cn1-binaries-${{ runner.os }}- + + - name: Restore built CN1 + iOS port artifacts + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/com/codenameone + Themes + Ports/iOSPort/nativeSources + key: cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }} + fail-on-cache-miss: true + - name: Install XcodeGen run: brew install xcodegen - - name: Build CN1 input-validation app (iOS) + + - name: Build CN1 input-validation iOS Xcode project + id: build-ios-app + run: ./scripts/build-ios-app.sh -q -DskipTests + timeout-minutes: 25 + + - name: Build .app bundle for simulator + id: build-app-bundle run: | - # TODO: replace with a generalised build-ios-app.sh that accepts a - # CN1_APP_DIR=scripts/input-validation-app override and emits the - # .app bundle path on stdout. - echo "iOS app build step not wired yet" >&2 - exit 1 + set -euo pipefail + WORKSPACE='${{ steps.build-ios-app.outputs.workspace }}' + SCHEME='${{ steps.build-ios-app.outputs.scheme }}' + DERIVED_DATA="${{ runner.temp }}/cn1iv-dd" + rm -rf "$DERIVED_DATA" + if [[ "$WORKSPACE" == *.xcworkspace ]]; then + xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$DERIVED_DATA" \ + CODE_SIGNING_ALLOWED=NO build + else + xcodebuild -project "$WORKSPACE" -scheme "$SCHEME" \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$DERIVED_DATA" \ + CODE_SIGNING_ALLOWED=NO build + fi + APP_BUNDLE="$(find "$DERIVED_DATA/Build/Products" -maxdepth 3 -type d -name "${SCHEME}.app" | head -n 1)" + if [ -z "$APP_BUNDLE" ]; then + echo "Failed to locate .app bundle under $DERIVED_DATA" >&2 + find "$DERIVED_DATA/Build/Products" -maxdepth 4 -type d -print >&2 || true + exit 1 + fi + echo "app_bundle=$APP_BUNDLE" >> "$GITHUB_OUTPUT" + timeout-minutes: 15 + - name: Drive gestures via XCUITest - run: scripts/input-validation-app/drivers/run-ios.sh "$APP_BUNDLE" env: - APP_BUNDLE: ${{ env.APP_BUNDLE }} - - name: Upload artifacts + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/input-validation-ios + run: | + mkdir -p "$ARTIFACTS_DIR" + ./scripts/input-validation-app/drivers/run-ios.sh \ + '${{ steps.build-app-bundle.outputs.app_bundle }}' + timeout-minutes: 15 + + - name: Upload iOS artifacts if: always() uses: actions/upload-artifact@v4 with: name: input-validation-ios path: artifacts/input-validation-ios if-no-files-found: warn + retention-days: 14 javascript: - # Likewise pending a build step. The JavaScript port build is produced by - # the CN1 Maven plugin (mvn -P javascript package) but requires the JS - # toolchain pulled in by scripts/setup-workspace.sh + a static file server. - # We scaffold the job, leave it disabled, and turn it on in the follow-up - # PR that adds a publish step (or points CN1IV_URL at a known deployment). - if: ${{ false }} + permissions: + contents: read runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 25 + env: + GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/input-validation-js + CN1_APP_DIR: scripts/input-validation-app steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + + - name: Set TMPDIR + run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + + - name: Cache codenameone-tools + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/codenameone-tools + key: ${{ runner.os }}-cn1-tools-${{ hashFiles('scripts/setup-workspace.sh') }} + restore-keys: | + ${{ runner.os }}-cn1-tools- + + - name: Set up Java 8 for ParparVM + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '8' + cache: 'maven' + + - name: Prepare Codename One binaries for Maven plugin + run: | + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + + - name: Build ParparVM compiler bundle + run: mvn -B -f maven/pom.xml -pl parparvm -am -DskipTests -Dmaven.javadoc.skip=true package + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'maven' + + - name: Set up Node 20 + uses: actions/setup-node@v4 with: node-version: '20' - - name: Install Playwright (Chromium only) + + - name: Cache npm modules + uses: actions/cache@v4 + with: + path: scripts/node_modules + key: ${{ runner.os }}-scripts-npm-playwright-v1 + restore-keys: | + ${{ runner.os }}-scripts-npm- + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-chromium-v2 + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright Chromium run: | - npm install --no-save playwright - npx playwright install --with-deps chromium - - name: Build CN1 input-validation app (JavaScript) + cd scripts + npm init -y 2>/dev/null || true + npm install playwright + npx playwright install-deps chromium + npx playwright install chromium + + - name: Install Xvfb for headless Java AWT + run: sudo apt-get update && sudo apt-get install -y xvfb + + - name: Setup workspace + run: xvfb-run ./scripts/setup-workspace.sh -q -DskipTests + + - name: Build input-validation JavaScript port bundle + id: build-js-bundle + run: | + set -euo pipefail + export JAVA_HOME="${JAVA_HOME_17_X64}" + export PATH="$JAVA_HOME/bin:$PATH" + mkdir -p "${ARTIFACTS_DIR}" + OUTPUT_ZIP="${ARTIFACTS_DIR}/cn1iv-javascript-port.zip" + # xvfb-run is required because the CSS compiler in the CN1 maven + # plugin pulls in java.awt during the package phase even though + # this app has no CSS theme. + SKIP_PARPARVM_BUILD=1 xvfb-run \ + ./scripts/build-javascript-port-hellocodenameone.sh "$OUTPUT_ZIP" + echo "bundle=$OUTPUT_ZIP" >> "$GITHUB_OUTPUT" + + - name: Serve the JS bundle on localhost:8080 + id: serve-js run: | - # TODO: emit a CN1IV_URL pointing at the published bundle. - echo "JS app build step not wired yet" >&2 - exit 1 + set -euo pipefail + SERVE_DIR="${{ runner.temp }}/cn1iv-serve" + rm -rf "$SERVE_DIR" + mkdir -p "$SERVE_DIR" + unzip -q '${{ steps.build-js-bundle.outputs.bundle }}' -d "$SERVE_DIR" + # ByteCodeTranslator emits the playable bundle under -js/ + INDEX_DIR="$(find "$SERVE_DIR" -maxdepth 2 -type f -name index.html -print -quit | xargs -n1 dirname)" + if [ -z "$INDEX_DIR" ]; then + echo "No index.html found inside $SERVE_DIR" >&2 + find "$SERVE_DIR" -maxdepth 3 -print >&2 || true + exit 1 + fi + (cd "$INDEX_DIR" && python3 -m http.server 8080 > "${ARTIFACTS_DIR}/http-server.log" 2>&1) & + SERVER_PID=$! + echo "server_pid=$SERVER_PID" >> "$GITHUB_OUTPUT" + # Wait for the server to actually accept connections before driving. + for i in $(seq 1 30); do + if curl -sf "http://127.0.0.1:8080/" -o /dev/null; then + break + fi + sleep 1 + done + - name: Drive gestures via Playwright - run: scripts/input-validation-app/drivers/run-js.sh "$CN1IV_URL" env: - CN1IV_URL: ${{ env.CN1IV_URL }} - - name: Upload artifacts + CN1IV_URL: http://127.0.0.1:8080/ + run: | + mkdir -p "$ARTIFACTS_DIR" + # Use the Playwright install under scripts/node_modules from the + # earlier step so we don't redownload. + export NODE_PATH="${{ github.workspace }}/scripts/node_modules" + ./scripts/input-validation-app/drivers/run-js.sh "$CN1IV_URL" + timeout-minutes: 10 + + - name: Stop http.server + if: always() + run: | + kill ${{ steps.serve-js.outputs.server_pid }} 2>/dev/null || true + + - name: Upload JS artifacts if: always() uses: actions/upload-artifact@v4 with: name: input-validation-js path: artifacts/input-validation-js if-no-files-found: warn + retention-days: 14 diff --git a/scripts/build-ios-app.sh b/scripts/build-ios-app.sh index 2bf7da8baf..469cd84188 100755 --- a/scripts/build-ios-app.sh +++ b/scripts/build-ios-app.sh @@ -78,12 +78,21 @@ if [ -n "${IOS_DEPENDENCY_ARGS:-}" ]; then bia_log "Applying extra iOS build args: ${IOS_DEPENDENCY_ARGS}" fi -APP_DIR="scripts/hellocodenameone" +APP_DIR="${CN1_APP_DIR:-scripts/hellocodenameone}" + +# Derive the iOS project / scheme name from the app's own CN1 settings so +# this script can build any CN1 app, not just hellocodenameone. +CN1_SETTINGS_FILE="$REPO_ROOT/$APP_DIR/common/codenameone_settings.properties" +if [ -f "$CN1_SETTINGS_FILE" ]; then + MAIN_NAME_FROM_SETTINGS="$(awk -F= '/^codename1.mainName=/{print $2; exit}' "$CN1_SETTINGS_FILE" | tr -d '\r')" +fi +APP_MAIN_NAME="${CN1_APP_MAIN_NAME:-${MAIN_NAME_FROM_SETTINGS:-HelloCodenameOne}}" +bia_log "Using APP_DIR=$APP_DIR APP_MAIN_NAME=$APP_MAIN_NAME" xcodebuild -version bia_log "Building iOS Xcode project using Codename One port" -cd $APP_DIR +cd "$REPO_ROOT/$APP_DIR" VM_START=$(date +%s) ARTIFACTS_DIR="${ARTIFACTS_DIR:-$REPO_ROOT/artifacts}" @@ -175,11 +184,11 @@ stage_bytecode_translator_sources() { bia_log "Created archive $zip_file" } -bia_log "Running HelloCodenameOne Maven build with JAVA_HOME=$JAVA17_HOME" +bia_log "Running $APP_MAIN_NAME Maven build with JAVA_HOME=$JAVA17_HOME" ( export JAVA_HOME="$JAVA17_HOME" export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$BASE_PATH" - MVN_IOS_LOG="$ARTIFACTS_DIR/hellocn1-ios-build.log" + MVN_IOS_LOG="$ARTIFACTS_DIR/cn1-ios-build.log" MVN_CMD=( ./mvnw package -DskipTests @@ -211,7 +220,7 @@ bia_log "Running HelloCodenameOne Maven build with JAVA_HOME=$JAVA17_HOME" ) VM_END=$(date +%s) VM_TIME=$((VM_END - VM_START)) -cd ../.. +cd "$REPO_ROOT" echo "$VM_TIME" > "$ARTIFACTS_DIR/vm_time.txt" bia_log "VM translation time: ${VM_TIME}s (saved to $ARTIFACTS_DIR/vm_time.txt)" @@ -269,22 +278,22 @@ else bia_log "Podfile not found in generated project; skipping pod install" fi -WORKSPACE_XML=' +WORKSPACE_XML=" + version = \"1.0\"> + location = \"group:${APP_MAIN_NAME}.xcodeproj\"> -' -if [ ! -d "$PROJECT_DIR/HelloCodenameOne.xcworkspace" ] && [ -d "$PROJECT_DIR/HelloCodenameOne.xcodeproj" ]; then +" +if [ ! -d "$PROJECT_DIR/${APP_MAIN_NAME}.xcworkspace" ] && [ -d "$PROJECT_DIR/${APP_MAIN_NAME}.xcodeproj" ]; then bia_log "Creating fallback xcworkspace for generated Xcode project" - mkdir -p "$PROJECT_DIR/HelloCodenameOne.xcworkspace" - printf '%s\n' "$WORKSPACE_XML" > "$PROJECT_DIR/HelloCodenameOne.xcworkspace/contents.xcworkspacedata" + mkdir -p "$PROJECT_DIR/${APP_MAIN_NAME}.xcworkspace" + printf '%s\n' "$WORKSPACE_XML" > "$PROJECT_DIR/${APP_MAIN_NAME}.xcworkspace/contents.xcworkspacedata" fi -if [ -d "$PROJECT_DIR/HelloCodenameOne.xcodeproj" ]; then +if [ -d "$PROJECT_DIR/${APP_MAIN_NAME}.xcodeproj" ]; then bia_log "Ensuring shared Xcode scheme exists" - "$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" HelloCodenameOne + "$REPO_ROOT/scripts/ios/create-shared-scheme.py" "$PROJECT_DIR" "$APP_MAIN_NAME" fi # Locate workspace or project for the next step @@ -315,11 +324,11 @@ bia_log "Found Xcode entrypoint: $WORKSPACE" if [ -n "${GITHUB_OUTPUT:-}" ]; then { echo "workspace=$WORKSPACE" - echo "scheme=HelloCodenameOne" + echo "scheme=$APP_MAIN_NAME" } >> "$GITHUB_OUTPUT" fi -bia_log "Emitted outputs -> workspace=$WORKSPACE, scheme=HelloCodenameOne" +bia_log "Emitted outputs -> workspace=$WORKSPACE, scheme=$APP_MAIN_NAME" # (Optional) dump xcodebuild -list for debugging ARTIFACTS_DIR="${ARTIFACTS_DIR:-$REPO_ROOT/artifacts}" diff --git a/scripts/build-javascript-port-hellocodenameone.sh b/scripts/build-javascript-port-hellocodenameone.sh index 1824b2bd07..46b6f428e9 100755 --- a/scripts/build-javascript-port-hellocodenameone.sh +++ b/scripts/build-javascript-port-hellocodenameone.sh @@ -26,11 +26,31 @@ fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -HELLO_ROOT="$REPO_ROOT/scripts/hellocodenameone" + +# CN1_APP_DIR (relative to repo root) selects which CN1 Maven project to bundle +# for the browser. Default keeps the original hellocodenameone behaviour so +# existing callers stay green; pipelines that target a different app set +# CN1_APP_DIR=scripts/. +APP_REL_DIR="${CN1_APP_DIR:-scripts/hellocodenameone}" +HELLO_ROOT="$REPO_ROOT/$APP_REL_DIR" COMMON_ROOT="$HELLO_ROOT/common" PORT_ROOT="$REPO_ROOT/Ports/JavaScriptPort" PARPARVM_ROOT="$REPO_ROOT/maven/parparvm" -OUTPUT_ZIP="${1:-$HELLO_ROOT/parparvm/target/hellocodenameone-javascript-port.zip}" + +# Read packageName / mainName from the app's CN1 settings so the generated +# launcher imports the right class and the translator gets the right entry +# point. Allow callers to override via env for unusual layouts. +CN1_SETTINGS_FILE="$COMMON_ROOT/codenameone_settings.properties" +if [ -f "$CN1_SETTINGS_FILE" ]; then + MAIN_NAME_FROM_SETTINGS="$(awk -F= '/^codename1.mainName=/{print $2; exit}' "$CN1_SETTINGS_FILE" | tr -d '\r')" + PACKAGE_NAME_FROM_SETTINGS="$(awk -F= '/^codename1.packageName=/{print $2; exit}' "$CN1_SETTINGS_FILE" | tr -d '\r')" +fi +APP_MAIN_NAME="${CN1_APP_MAIN_NAME:-${MAIN_NAME_FROM_SETTINGS:-HelloCodenameOne}}" +APP_PACKAGE_NAME="${CN1_APP_PACKAGE_NAME:-${PACKAGE_NAME_FROM_SETTINGS:-com.codenameone.examples.hellocodenameone}}" +APP_BUNDLE_BASENAME="${CN1_APP_BUNDLE_BASENAME:-$(echo "$APP_MAIN_NAME" | tr '[:upper:]' '[:lower:]')-javascript-port}" +bj_log "Using APP_DIR=$APP_REL_DIR mainName=$APP_MAIN_NAME package=$APP_PACKAGE_NAME" + +OUTPUT_ZIP="${1:-$HELLO_ROOT/parparvm/target/${APP_BUNDLE_BASENAME}.zip}" TMPDIR="${TMPDIR:-/tmp}" TMPDIR="${TMPDIR%/}" @@ -65,7 +85,7 @@ if [ "${SKIP_MAVEN_BUILD:-0}" != "1" ] && [ "${SKIP_PARPARVM_BUILD:-0}" != "1" ] fi if [ "${SKIP_MAVEN_BUILD:-0}" != "1" ] && [ "${SKIP_COMMON_BUILD:-0}" != "1" ]; then - bj_log "Building HelloCodenameOne common module and compile-scope dependencies" + bj_log "Building $APP_MAIN_NAME common module and compile-scope dependencies" mkdir -p "$HOME/.codenameone" if [ -f "$REPO_ROOT/maven/UpdateCodenameOne.jar" ]; then cp "$REPO_ROOT/maven/UpdateCodenameOne.jar" "$HOME/.codenameone/" 2>/dev/null || true @@ -93,10 +113,11 @@ done STAGE_CLASSES="$WORK_DIR/stage-classes" PORT_CLASSES="$WORK_DIR/port-classes" SOURCE_LIST="$WORK_DIR/javascript-port-sources.txt" -LAUNCHER_SRC="$WORK_DIR/HelloCodenameOneJavaScriptMain.java" +LAUNCHER_CLASS_NAME="${APP_MAIN_NAME}JavaScriptMain" +LAUNCHER_SRC="$WORK_DIR/${LAUNCHER_CLASS_NAME}.java" TRANSLATOR_OUT="$WORK_DIR/translator-output" -TRANSLATOR_APP_NAME="HelloCodenameOneJavaScriptMain" -DIST_APP_NAME="HelloCodenameOne" +TRANSLATOR_APP_NAME="$LAUNCHER_CLASS_NAME" +DIST_APP_NAME="$APP_MAIN_NAME" mkdir -p "$STAGE_CLASSES" "$PORT_CLASSES" "$TRANSLATOR_OUT" bj_log "Staging JavaAPI and application classes" @@ -148,24 +169,24 @@ fi # Both launchers work - they bootstrap the implementation factory before Display.init() bj_log "Preparing JavaScript-port launcher" if [ "$TEAVM_AVAILABLE" -eq 1 ]; then - cat > "$LAUNCHER_SRC" <<'EOF' + cat > "$LAUNCHER_SRC" < "$LAUNCHER_SRC" <<'EOF' + cat > "$LAUNCHER_SRC" <3`NI}|9vt+=~eaY)|ux%av6UrA2# zIcIivcJ?>lnM5fmNTMMVA_D*bG-;`?DgXd9l&#f8(U`n4F32?75$ON8BPSyqv5{j7qDI_QMdx& zVMN;oO`X6LC~xTNw_QH^J+9Rzs`S3{D2xfSggVvJ^X-ZMG14fPmO^T$qOihq`W>QK zl{~@d*K6T0vnTYJDz==6wZ7dS8LCeutsYEu_%eZu?oGm|aOi!BBYxau>?&gfrB@!r zVDy8DHnhnSc7gH<8c1G@>TZlo9?Aip&F1@>@Vc&wyjb*kk@urmGZgFiu~$8B)_4aV z1K7P4fCS&KY`81^8{a90;kaorh%io~Xx8dbxZ$>M7{?6t+5~1h7QL$~xiPePMk4Uv zcbq6;#kYzmw6Z04Xo1yA68*I&VGtJp*Mps?IA&4yf?jGe3wKL}pBD(3vmI&0>Vk(pS5rE!OhMUw-N9#fNXpyF(Rn{m_C6= zO$(-G0B4rQ``N=nei|pi0d7}0Ku^I36vP4kXLQ(_MQ_H)Fo0&v|GnF`O`5RY2mtT{ zzx?0r?ijIi|;PYG|U{16!SF-%*njew#5?-2B67cK(QP;RRX z+RlE7{EuPrw$e|wBV1fhi~l>~1 z0w_5O-*)JMfU?gF+p!2;We^-2dK#n0G)8hYIanDKwB(>=OKPz0BCem*s2-yG6 z3O%%@iZ^3;wEw$><{%Z(>+fH+{v5>>WhJOi^riHe0tezU1rWQ4I^V1>XO{vlXYY)h z=ghig$x(j+w=y?X`YJM&(M2ZA@&XO8qX+DAgZ|@xHIi(B1{@8yj%px|-_`g4)+o$G z%)e{f(|@G@v)=QcpG5j<+>HHXx3pD27-0Y#6+sKEAeYDdzXiAvFsRGt$TlLhvwaYcZcJ?fq^ z{Z0TDP0AeoAc6xxXduB4S(`s^SlJ!@soP((B_W#)qoCF&H|7j65njtA8qc~E=ihXX zbyz}`21bWwR3+~ax@E=c9&*I~3k1R>X#oa#HPvrYJFi&8;`))xz$Dr#sU}1WoAbT2 z1d_*`_VH`h^n}Vu2FiYtgtK-q6kQD6S6W-Szyag4iw6vK5MEs42 z5&or)@)p^@50~=P6>cv)GIGi`-F!Q@LF;J1gfcH+5yPh33%B6XXGuxew8)72$_Hgz ziLaOw!Tp&W_}(y~Q3xPFDr(~5J(8UpsrC}%Y?5+W=b+AgG_I#kyYC8&OzVtA-QX>? zjJlDe_ihjG$IVaxX%QgLUi0ihQ}o(NoBjl?#1aunE9Rgs`izjYtMJ}%5DN1@cR+Lb zOWeK_3Uz_B(ei)18K@(rDXFBLKI-fyy`d(tV&G!Mn3Gs7yc>Qnu3$QD7#S5xbZVzZ z+Gn)br>O@R)QA{=Km{0ztp*y{uHPFYnIoYiT)Pl79-ilCSXfBMciHwEp*^IM^CXz& zH~d6XPWK~Ab7=|lE=If8pI6Qmv`1V&4$%19q`ZUKqxj~pQn?sHN;mdod5eGFrZxiD zlMpR(VGgTjrfMfgqu^TBPJT*4X`KBXsd;=e^bwljiy`*?yo@ipF73eQpQgLKG;*Ag zB7T8X14;BBbUskElrwi?(HEXbZgCmCaqOLO=DQz=jaB7AyDWpgL9D$|7OEeKf0{Pdx73mt4aP z>hv`;(6n;VcQvq@?l(TLC68OJHwK$-%rj3mbuJ#qi%-Sd;BPL?0LnlL7E8mIT88^y ziqfu*qOn#_+Q_5&i3LJ+SYq96~ji`k?pAqA@7Gx$oE4ET|bcJ8IQ zRWH}rs-3$Iz+QXBqBBIxKdRbpq4_Z!V4xuzQlsl~hhsf*;JT?s~fTU6ss5h8?EanB<(uPG&2r`Ip3imR6c}p@7!78NR5n z7^~c$#@Qga5Dj_~RT;Hlxa#W-_q!D|djIDTN4iY#`@(@+x*cS*F># zZm{n53y+GT zfSi!1S#Frp*P3e>#XoVuZsLg4mX+LlpWxlToZhP=$OIK&3=RV%U@FAe2HHI=6}n-V z*wH!XW&IBFH6Bg)jKqX=D-806aud8{y#UE(Ei@t=Fux8wd)0UX{)BEy?RaP<=?X`0 zjUoroDlxx{H77HEy*yZUio^k#=q*me-*72-qnad4Y60+UUBoh}Ek$z3e{1hayMIz< z9uP^DREkCISSF)|2fn6MkIY!rrdMcyYRa2*{r~_}xzkU{`X%rbfD5Dl7pl>zebU*kWaj1pP?DG8M$Kl3x!acig82pH#<^ zNMr?lk%pC62?=0^AnqWu5S#M3C398}Y1j$t;Hs_kHUgIiK(_g5JvKC`Dx8ZR43bJO^H z8*M;km*rtVxUJ2iW*V@Dn8CMaFn?L6A{i2UG#${veK>lH!zdyrnRrc%{eJjWu9f4_LjtnuQ^ z^44x0H>2V=fJFrBJboZD2xr*x$pdRdMDrsd5dXBJ_cwk!emOOBNRlO)**g9VL^JD7 z^d+TNLua1rxw-d48xuC>?^$q=+mr}l+%I1?FCb5addsdPt$xjLARccc)O?Pid-YDT z%tskn!w>09VE$voij1?5MPBq>c7w)?D1C($7ctz{skij9!i6^v)?59vlqS@KEpx_& z@Ck40Oy)Gve?%3d!!GrgbR~v2s&u1@8V#FPKyl-sBnlJ+0v zAK1Qb2#e2-2-s7*#6FuGA~dTaoy{6&n+SOW;0N6!{`3S$B&8)GR;Fq z`>PdPl75TjraNOkNKc1fIiPL$Nf%YUePFf&8YTWY^Fo;5ZtkBLZpV*oY7bM&T9sug zdD>g_kF10tja_fRv!(ah61h)aV(^>1pF1YNzugID;d2m)pwcp-qad>!bV2Yg!Whsd zom!-%2s(8*#_!B^B4^FV24%B3{*#3LVpN+*Q+$yb7)b)oEcJEaD8i*#|!c^e^YgqwF z4?Ca$UucZTF|S_pxQsfAG+sZI+RPG3wRB7=0m=jpEaJxBcbU|=vR6J&(Il0Bo!4YY zHWj1#@PU_}ahtGyE~o?-Rk8!x(hrAZMiA~04~6P*>vwl0bSA3)C)R1H*rr6A$8ME^ zhf#ww#234kVD_-+SM;Ur<}zT*!lg5?M91Uitoo#?EQ~$rhy%LCIhARFdkPYRy$sz+ z^=wdHKDC&;^})%?YZ;?VJvn_@ebFm83?zy8bCBKiPE!}!_ja)I+M18LV1lCU6ZoR_qYjGu=N(%?VE8h zl%&&~>N9LhhmiARG2=p7z(g?QEYL)lK%fMsv3$m}QWxv183D%P^wq7ug(T%{Md`HL zaNa=;c;W=%4enI#wlO&{M?`G$UfDdwJdtk#Fv>> zD`rQPAjY|c{;|fM!T2k*&w`QnUp6sg^Goo-$4<+EVdn^uYfMGV{H{+&z2y}HNr>s0 z69qBk=DMHH&}B;V6MP1LFvv=CkeTSRQ7yh-+R$Y{bEiHv52Xocq714RK?4LogUBwiW=aNrZy{dqTASe>+RmE95bt*9LlZDf!HFJW<#+|cR zm1%!cql(Cx-lCS3yo@?lw{qBCo(-4XS#3Ip1Y73UXBuo!scC%k%(`wpNnob8i4kNd zW-XYk8;EKGee{c5hDYV0Ww{j~O+TsF^EScltLi}?K>HdVU`1h6ilmI>-x}CLHvm*?rpQ+k zW2$(np)E4ZxV7folNvdapbPagA0rZ~ma8y;wx>4JFDZ`K+$k(&F+nS3U#T=VADc}cjg*{~q<-(xpxMNP#_PK;#aq3p7XUF~KR1@lQ` z!Fw5#xT=4}iv8s4fUT*@L7$Y{;7^FR5<$&Jky1Uo>()=H>)x)4xu5W#4K>i`-cw?w z{VhI7k#Kb{hLwUkDvAy?Fs!F9i9Pq^nQ-mhZi&9FJ$VbPGG(iKGsXSNuw7*=Hrn6* zg^U{-cedXU1$yza8LM*f&0DknbBRBC$*TDKeRQqYN_H>i5jJ9Ynu<{c6IoJQBpSy2!ixo&K6#uDp*DE9hHN|FrVC`KI! z9odJaj0=QYecF-1EI$Xil!yLCvrgN`V|WrJHK?k*kQaSCU9z%B;oTI*>Yw^qyo6Ql znwHc%5u_kXh15&pb+BHpW5h-TeZL8c0|sCo1u)sb0NfNrIAE3Wgex?#&-Bxr%n)(F z0X@idnpA0_Mi7M3q)5MNxs7J%j;i+qt2+MH#X)KaOV_FWC#pL`C9_tnM4-VlkL4^C z6gP!!f0j*3>wt?h0o0g!AA^yq4%PEw)2kpaq5Dl!m`xjFoq4s6**YHAzXh7-L`PlF z`zz+zK}m26XRA{GOGEvOy*95h4ly%}f@6Oulix5S%^TOhkopr6QW+m|13hgwMJmS{ z2R>a*8X|5>eKRK*YVQhYK*WeET}+CNfMfn@9#W+e+!EC~_#=ILF*@1RV!_|1aN4-% zHlOgKM_!2sI{H*1DlSk+QVI3&<=g^32n%|c)(3nTyWPs@rc=3prH13Lmq=VALtMxa z! zvmwDMz-rLbT?CtgEcdJ_M+i9U(ZNR1E?5POghryNsxKnr(?~0J+k$w@<}PB3VShuV zndCWIqD}2qp1LdPi1~n+h~=vx-zp$8v^r7%DhvVwk{~EAFVKrn0K2@_Zy4}JzMX+a zD???BY#8u>yBJ-F^|cDrA!HmU;i`{c-l2!Iu)e7q+YQyfnwkvtqtu%@cX~5Ws`g$x z=@u{PmgOr0*z0xJ;)6aME8IQSz~CS)=0{qOK3MK{nVT|s^-GUG5gFsj4I=6Qd}o(_ zxtZO)URnr&xy(B7+D0tdPWZ;}>L61EFDA?Q+Z?V=U0szW2uP9X*fs>))|dI~Vhh=@ zGVCzVU|T;tEEyYv=A{FEJsHPNA;h+F0t`dt*-&8mPBlSOoGBI;65Y!+xcc-Wkb zR?C)0#%nmk2DTL^0%-R8azn$?A8P(12<;4IHm~wf;#{8oa^@&&Vxuoy*~7uuX{eCp zG{9GF?EhH*85xPO1@1s;EM5Z-1+ohKtAKW~LsJtYy|pVCp7vn=#u*2?4Db%JfMFu) zA4+R+dH*Jr3yA0t$T`kaBDz!+&cGhDX`dQ_>gcyWI% zG*noe=1m`^CJA==$%A3T3T*)!W)!wOmMD%v?WER)=MqK7(-#~QAbf+lz==J&XH_D%4^^WO!^y!3(cz#)iBsR(iGHJnbK-$27(msw23TvwN_#5v;a|?gMd$8yP#T=b*xHUI|S%y zM6?Ur_J2Bw;C>(bP0y97tg0=;h3UgM@Q1ir`}E~U-0pix;>*6(1;g5^W!v@aQXiL9 zoO5FNqVa0r-K?02uENY2Emba?k)o(N#;+c6i}MNd)CW!-M88rITKWBPgqC0gecS5WtEyZ>$%?cYeL3A>VW`_)Qme^SN85i{Fh2I^T zynm(6vm(tG1S9y&8AjR+E8NibTB7!kVw)fB#>Fyfx&f8~IOtWt@?VZZdR=f|F7J>b zTs+U~3p%V#TaO8v0e;$gGB%@o!B)7MLVXvaX;AlJ@}3F6uL44_MVoIE%q?RK^T+4J zOl$FCIKKM96ppkGEFB}!!p%4!T~5#)kV4Z`(X)+abe7af6lLkND{s!qnkJ{HQ>Ap; zwR4o2sh<$;>&?gc{RIY$%-ER1Tid|y*G?fO(n2;V=6X_uUUSxTA=WuP&^vg_mnWL5 zNw$pcq^;xVLOm!-l|5nB#Etnxg-u)ooc5cm(0ZbrD1mKXHuL*Sp z4!iSFkbR{Co6`;0^Wo@}OYmKZoO)U?mx3EVT9Lo2cDiZ4ZR^ zjw$=KKihtl0POL2H~s2wA4s5C^j{}p2X@xN*OwudRnpHJP8``bM~?V$vjqz2CVvu? z)HM&~vW9t`z-%muVRT1@zh%C-xG1jvC?S~_OvUSdN_ime(?Py8)M!q`x7Lwd^c_Lc zuZKI+8FyKLRmlr#l!C&@RwE420T@UD8!=v~EYkSZYv4cnVnlEHR(oGY0E(+Z<0Xe( z<{3qeXkr77mo)A9gex09mF%LG(yLCY*Eb1NAIa~Q-H_p-;{T38!CuhjLHEhe?{A&X z@M?>#cB2_my18=IB5lUF89M>J&!fv>nxLNNi}RO^nu4R!3sgq!(UXUR+`$s|altLF zJG>xzYyo|BiT+kvw+=oS+~5pZtk|1PD(?f~HY*sTbOJ?GIeaL-W%ZBU%1K)Ac2mfI zI&9d{UhBJH@oc?StUh{S&K)Jxv*9EX3B*0F3MmNP@v;yvhg`k7xw*}3ZA~XHR;_fp zGgN8Tm6nx7Oid}+tx+RF%amYIP*5O}YjwC#i6H_H*b)5Sh{d;_smC>HcjG6#mGC+q%9_a@_?jBl2F!=8b zx=k3N!5@hJbX0bXB%-rO@S`2NHE#VJ#Z89Vac=X-M%e;MKD=E!8w6Z%0ZXtPfW~9V zRMmPN$!J8}^}f$8U-b&iRVor?O0NC#2L?o^@&se9UIC%TQjU%+q8Ltp zkCh*;HizdG1&4uLw(XBUz)`we53=V4ad#HsJ0eFPY?&4R1kPsfhe5CBVY_{6oJVxY}Cg7w?R@r6oy4MRa?6dqi@45|UCidNy8ODdr4{{OOeO z16I@!m5N`_cSjvgJJ5MYJDtqo#;n}j5*>Qf8RHEWlZ3IRqrB}N=n)TnIceZ)>r3CY zwGJ?zf0(#F5w8*#ozc!&;KJW}z){fGv04{ImRMIIyOriSk zwTYgwx<;qX5?I>@dAbGrsJ!Y)3t|H`UO4kyu&5Gij^WB4ed%kHSKiwDd;VPbQkU!~ zcK-GViheJq-Pc1j6^k~1A0CpYkH##Tis!9WDn5#0DA9g|crJc;kh!+|aQ>K?lRnL-Vzle^vK8vEpChyopz@iUF{h=FYoUim{-q>ZU1!PTdGUX zwYBGy{5P+ux~`B7H&$9*%TH`|TCjH_SX7T;*G0->KfFXO7k*b04o(@iRFTb5v zpkNl1f+tgb@oY0OR>?KLJk=WPUSsXNM&VTU0#W8997K4-m>d|Q?Tl0#=}XkyBG>|% z{umE@vJcibohQZP2)b__wV^3CDdXcjbfunY6J=;vz3u4nj7=Kt8$$u#etWoDk*7~H zS!p!izw-*kSg_$5Wc|g8DmPyZW-9r`DGwP&t9|tF=Td8v>{IGn0raiM8-WC!n()|l zS;#efcrnCavi`?+q$>g+g2olbqN1W);eb3<@Ee=~Xa{@UcN%6L6ayxza(LCwV_tRj zI^krJaxu+Mg`(h6KU}vTIIM=0ah22-p;i$y^_%zeVW2C#tsLW1`+`(J(+M)G@+EJ3 z`mi2xT#mSIOv}*zb0i0?Nkev*WAj}$*!=PO`)A_8cyX6!V8En%(_%&A&H;_riKB2o zhawnb)L9GSE3z5PRH)(t(^U*!fn=^|NH@l2lZx7BR$XfM`Qxs6(A92{=CZ z;rCz~q@hGI#M69CQrm3mjg1XROjBaSB_JT!c})Ou2>b7{ff61AQrAeH6uZ@Q7zgww z53E|Nwt9ojC<8C-gB^%k0Afz%lxLezljk#pd;q4okH*}It04t=Qo%xzpjsprWDOT>JP(c)WiLZ&=$LsLE3~>SaXnC6PP6Amw=`2oY7J zd&ZNb^4M2*?+@pED%@V2`+V&FPo~J7l0!5pIdZlKzep$oP=m`c|jwlyIW{O z`}GW4*?PV}o%7R77g=m4=#l-gKuBr+hd$P(%a&(c6??ro!^H#80(3`3Bu3I;j8sC&+Bpl8&cr%VrT23Hx=D~dGj4YE?^OwO&Xjd!>nJ^jfCeL&A$lP;?meljtm zKOpsCqdE%_l=ABG`aZC+MoE97my4F*oQTk2OYbR`fZ(-zHA7QHIS!- z!A`g%nYb*Ex7Gbj`ufTv!q!iOVv)+|Z?o@9BZB?CO^gy9amtc;iD-lYNDz?b|j+4kvTQ4qX1; z-?Oo@iUb4*^Q=3;NIqQcoCv=%`8!x;*en-jVW#sZ2WWbwk#@f>JuOLsq9A_%^73*D zBxRPL=+dbDy21nU;|l-wwyIMjL7hULG46aFMtXCe|3S^jJ`XBwdH!{0h8V#nk$%$EKSDwtfs9IvJ}LlPzykNCnt=+ z8r?RR#|;lx(1dl~Tg=uTQ(3^op&IMal5?S;20jUTu2)js`;wvYXrGXRp&_YkIwLvi zkhkro#KF4M9=%ZfIFC^Fd)6)k-bL&}k>d$E+dHmrQ1+4NpWdN(u54YN{weN%zQ7?pE zt#_p8cYP-hEvTzYuwQF!@O%9(At~u~D&9B!sZL;u+f+_>XZ{zV%=#O!@AilA%k=qH zQ;zv~dV|}^oVN=Jr{>4%v@>kNkO+R}V8lLMWf+hr-=Hg`ve? zSJG{H1t{6AuPDi-4&bWwWtvTqW6y7medT86o1ykzRN@ROKF}7>_J9ch^y0CY%rcgJ zuyV?^3sQVK)rl>wYljNOwme@x!jypG{Q+KsecI$vCv)4TH-m1g9eJ9{fZz1pa*q$s zw9qT#(9qDm1k1B;o_r3s^R(cj5`EeeND`FkJ{^$ux}KaDxHR^ug8nSvuQ|#4hT-1! z<_4o-Z*E>*p11bK;;-!#AVIs?aGt=WP9`I4A}1~3G!3mqp+5OW4f!*{=4f6-J0r0?OM`y z8=YxPx~+0fucQK=iKOqxl4@1ztE;OJZAga5iVCyjzG%T3J}~uz-|Mg`U~<}G?uiG? z$P#W3JLx`z)1)-&$Bw%F&@B1r0mjSso4#6T+nzkSQ>&7pGl=?`M{lu}$%tP!LZtZ~ zCm%wXVEdN4%o=5w@g@igxb-vHHJgVB{qXScC!qM%o@T~h*Ku92$9KH@PMAZmKx7)Y z2I-)WA7~HMFG}Q4<^xE|CVyA09}M$Hc&j#2RK_as&YrN;v0jI87HbLUMS#O^M2!w3 z8Q#4CQ1CC@(_q8tLm_RLDGe(AgQs6zObDWrNAi}2#okgiZ+m}g=@0wR)-=Q~yhvUj zF7`T(@`}{xXJ==Fth2G#r(5mk|NI%r=CYM4o9}+wjS+ZBdb~OG__1pla0%W;NMkfz zN?Gp#K!(pw{r0s|eTJ7VdqeKyd(6ip{^VmmW+5btGEY!^8J~({S|IlLJ03rE>)17|>0-(g}T<*z;0C zE#RSQ=UUmP>A3BSzcPYSDUT1LBR_UEDnT)z_3WT^KJqClz$J9Gk5=R;0i<^Y-zyj8 zqXtN>^_sR#U`^XbxTw^&`i)@_haWNXo%=Pzo z&|3z7Ua`~o5reVrxfi$YvSyzuDfu)-BRnPI!*6modCun??b0FVqf3--&IPH))SaUa zL83Aw^r&2Ro-y049d=y#{@@oI>jN)t?J4)=G^}#^SHO*2jL_lTAUQ*9knN#&XD9dQyONK-%+XjJX+et_@* z)DItSSpsg?-WFbkhnM>WW8v<+nc9vs>u>u;Li-p*e~LksNQFB%Vw=gecqy@cM18L8 z6v;_iiZB?Jhk;)F^>dP#CFJI3EZ9Umqwi!tD0dnL?>k5Hl^4YO7%m{8)_G~eLzOAR zYuA*t@Tnnz{Wb~x_N7c%`e2anR`%6t?lSel+*DB{7kg%DY4nH3=2#rh=I>rofvKcE zP{6afLj6|{G&|t?338UBZ?Yulp~#&A*jW7nFPNbUN=e(vD%D26kuztNGne9g?H+Np zR@;q}j#dpL%O)Tb#n|p-h8#E)5r*cYlAuVQC50rmX2g6JxyrXP>w~RGvu@pOQ1b2f zQnYriGBbOs+@r!kToDL@jRJH_C2XdKabv$>e8Am~yIqL$uZ_6q2+Swih-o{rW|ZY~ zk()OgPiI^LMDgo zVn`0@vZU|P;B{y50DjUoc$vAnbsUe-Dj|ZG-?nIqd|phrT})wS&e>u`wObo$WMmxgEat46#h2=J+&dZ`Rdk<7d)qhoDW{>+lxRK`Yn$))zmullgTlKC5>$=55gvtE>5V%s$~i{=OKO5KSuM7o4VSYT>oU^wD!EfDhAxL zTni)|jAxkQ!{;5zYYXSQUGWGpj#qZQ

g@MUJI7HpSpMR*={}r zms%%}9{`@F5I_jJk{`~B=Wxwy(*j~K=xMf{q+O_0UTo)N9@6jgpq66uzAr_J$cK|i zn!gc=*L!V%Nc2IZSd=IY13YUX0h|%zQ2t4uZYN-u@ZnbB-uOKRhLyMc25e$*@ z6>0i;ixJTx$!!_Z8z0V?GoCXP)~o^6m8~1sNgec%&Ip$d;?a2GoqbE;-`wDH&e&j! zFBPM2YYWjnm2rL+FuEd<$qhrj$sK1LGVmGhwA-hQSe?#synCO`CzcX0R=Kq@c%XyH$}^A-UHCNW3?^aF1pxg#!cP%) z>o?ynC%t1TFV3(KE(yJ|Sim6gBm{{;j=Fw#eVdk5sv6If+II_JS3{U1M@WIP7f)~P ziV$FIj}R?L;%dMwDkOzXokaAO!*tg-uw8rc5z-}99Poz5U3#xlOJ#+7SI2d7A^chv zMs&g9R8U}6YfDRzi+g(Yjq-abTo1tvmw-)S^a_NRt}@*}4nXT&7t=rH98T+IB)^$F zlO5;_m!m9@Aue}&ad>RF<3W2q?L<5u8~Vi-jVA1x>HE?Q^BKErI#-yvq_Pq)XyDg` z#agTu_T(6h8{H>moII}{)f;YxeAWAd7|GFDl6oI53L&hAmt9xX5aC?Y_blt(2PW{R z)`XzTXRXP!|qw(yJ>=+Q6l={UdsA+9G=4*WKoP~`(@DsI941gbSL&Cl|e zs*tH<@ua7##}(@;F>@wc+;a7MKb#l$0n}n4sfjNqeo}nlS5_7TZ4k6)6LDORw`g8_ z2=NuZFKW0f6U8vfre3j2cR%}c(+`9Gk3K(=f^@6v z0)q2p@T6F_`Ku>`+q~-Tp1@D)4}yImAdvs#?}iMP(;h@u8dQ4Sc`{#JYTEL2OyzJW7z;opj-5*6XVjdR(9+rapm#W0;$dbDOZ?7zboikj5wx57BGoW>9earkr= z!zsbXb#X{$34R&w8})iKZtdLsYmkaNfOTy#bu$WNnG+S-6kU!i+L0H<7yEAdb zrv$%Gj}$zT-zhL{g}xnfUZ$W|MFzB(z}VJuH=BTPt?9AuYoez$U}*c z*=mHOoQ&hUF@|qg=z@QZ<9k70YR{4tEFsW%i1pf*%Xjp!*<7WIS8TRg8HH-xE`oIn z=>(OT+CmpWlvz?2XfnCEv7GRZVkwYt3JE~Z+@YFbtho#LQV$Ux`WX=CXm#A0;+Pf~ z@&JRd-^9T55)Lms-Flttsdkgjj^R|>WgE)z{(sS*cX&t(A;k?yBKQIn?jQ`Wz~0ZM zdm0mfNuk@=mk^AT_L$^`?OeC%U9k|384>pcj|VG1c-V(pcLR%*&e22#Le(D#&l`f@lm znxwMZ$*dytr$%@5pH~3oZ>9~O2}wb}B|Q()G=Ez(-8=X}&d=ic6K_{C;*81*rBrgW zrs6yMcKr?BcNqLnKn#2)93=SklrwFyhkfN=4NKRmbZ>bjp%u;SfLf95(P(-I-HrObUxDM}<3B)X6;g0PZ_e(4$))r` z-o)R8oOjAJ|2GfQCjj%tlVxLZmxTF7=D`+3naBGh?ZyKvg?@$VDP62^Pet_VP3mDc-M5_<6iHX#4tiJWStgOn&#(om@+%>RMv&h{l zDqTJDNAj(MVPLX^o%1&m#O13nmaBmfz z{iXW_X_uysS=#(IDNj-SRc;3<@CK*CIvY|d>Fny_^MB*L=o!bq&>72ATEmm}7rD&xl!vAZ6&yw91$}$ zq?}@1;10QYP@p{HMP3OsfpS1>TyyDhdAkm{>3_(k zeiIx9L5cX`tk2GhQl}VSZT2k4z={ zP4qZwYrF?EJW^NuP3hw1?tC`ZYZ&z+aK`@(GUOP}5n!Iug@ntj@x(X5laT}*UsJ80 z+UI>Yz9~l^^JO7VfdG3N5)kE8C-~z0Y;|M@%WXAS0cDi}DqnU$MUt-C*wAS+gxpXC zal!r&zoz5GmI{)2e!R<=@2c9e!%TSGNiLpOk?8M4MO|7b6`k+FToZ$|x~P&V<-BM? za}VEx$cY&%^W%6lfQf%slHFSseatgge`^mWq42WzU!0hhyHfGn@=d{I7(56eY>qkq zy4wop-qE|hwhF8_VEAw z#?Y`%RrTl#v(ZE!d@MypfE5kgxTl)MRX=CiiC4Qo9f4ie1s<>RAYu3sMj5y=sC~Yayae2tw|jUU+&3 z#L@jSVGur;tZ~<#HM$(gIP^YS5RVo@>c0@S1KIRlT@h}pHID^t_GJd5Gb}{6TcGY@z34T@N-Ud?Yn=kA~{e zCM0%4)W_4Gw%*}eCJ)x{TYX1~8I`0QvC ztf=ZYf_X-eu)0P7J1Ln^t9DNSg(}rKm2_<@Zoh3xQ2arUOy>GNcVgZXw?J;J1S{R}Z)5 zoe?=5n6focnG_mUFkWVvJ9OLb8$nRbX9FEN1Q8zqwP*pXq@zszs(L_vMdj)|Z*8*x zRmU)YT!SToWS(EJrJ-jyk3wF77GttZ$#SbbQAcWA^>cTl2cDwfi{gIX6Ze%&mtF6X zCa;qFvFo<}A<}vViFi2F_tc9%|95YQEKumJiF5Qn)9nUNgM;5+AC}gxzq`9P_z!vi zsn#DND%&LcPmhD;R4#d%%gNPMj2L3_>?|Ac=zudZeH!wSqy#M*o0R*X0w{%t93mQGmU%)aWxxWJcaVxrV3dt1iB717Vk1rc=*Z#lH$Nh5{IXVYs?8P9t8(d3M?4ccqhZUy6c$J!zldpFWJdK4Zc>-F$YyB4g@&}5is{`RWjl#$AjOFl*eIYGRZw*Bs}BlZ zb9VJTwRQjfoAMDEZtjE=5`Zo*9Uz(KiCcDAo_F+>|6^D8Es(+O`*(H$fr%y1+eY_& z)8p)HZ(hD4TCzW=>#0}r%a2lxss@`y`hUv=!JS*rQ>Fq>+Yu{GJ}g;s*{!YIkRs{v z=_xc)noJnv6Qm-q#|%NEzzS8zuLj=I4O$X1+B|@8NoICV0l5~PFuU}nh=_`d)#=hp zhd+NG8k?(PCOYL)!>2J}&7rO5vKp)yK4Ns`jsEphSWl56)BJBQ09f^7M&00bWn?)y zq>%6vQl;D*O}P7ot+-v=4Rvz`LSb2kdAchO>>_mcv}=PKCq9-P5d6a`{MPTYaY}X-C#Y+AxWEwrn&wVNK2SJ zqWQupPm#_$eE$ws?4r>RqU+LAHifr3gy&5P!=2sXn0-h_UZeAkJ7?HAx08(D=YcWe z!>3)y7Z7yiJ(L3JsabwR1s_d6&VgSX6f_wivPNLqbg@!>9YSoH+uH7yO}7q0NZ&A6 zdYV(*R;eIza%DwYQxh+KkCmN$Fv^rhtFbsDD9HR{jJe$W;qEYIJgIPyea^=iD&o-! zhMdbuc6w7Xms|g*<*)YtKv_B?`YG>gtE@!FYyb#Do%BMhB}_cQR%%aPSYk zfjd6A?TF$=Um$)EZ(&Yv2xB1O{e>$VcoPG9M&w65=$d#c}NXZWq z8WJM-FJg7(j`LZ9w~|y8J~`(7IV-7RQ*QDXFx3vj`dg*!h^>;Tc{wGao@BhPvG5PR zX>XI{3@uRY3-Imue`rROiRm@H-&ZQd?u{PiWZUQ^P_i#Wz@ri~Vs}G&2WAhMQRTXS z_GpdU>es{nB8?!a)QKr4c8w*tV|*E}ubi1(w-g z{-&H_(udRbUt8LGi)Pn{^VgLMeQf=gYI|+X7M)vs&aSSA$}B0sJ}~A|a|`n$YOFN+ zWnY_p(giNsbAtjR@%{QO=^i549o(ewE!D+6^T`QGg?4>#V`C#)c~JxvUYxW*TPwJD z`O5Svl^UpG`5e7CpK^c8q57XJ{o1*}7bOn~o^$>Cck?R9=?AEwzvMMue$p(J_Qc0d ze;3RZx3rCs@fE-B;jWyIVj zoK)oQiW6mU@uUH!cteAemN%yF{nZZ_%9|p@5#8U0+vx zgLLg?&)jbE>07$Fkah)V`5YVpiSeF|kO?7Ih(d#kxv44AA{^bNmf5Cam*~5n12*l2krGk8XH}HyorpCMqtb< z)2UpzEo_pH_?1!gK8Nng28ki%>#d3+W==6s%_7wVc@oFSMXeQ4HR1>~*M!j3#iZOAyj=RU~>Z|`t*qbcV(vq02ww2k0#57aGT!@4Cu@+@s zdB$m5oAmK42M%};;PiC<;=#Up!M#m%ogohuVwXWAw?d_6LXJP>)pqgOf)S=MOMR(> zu)2OVN$HkEq}N7SN{yL1&LkA6W^d}LV+NRs^>l6GpOnbNt|65328XrFlso7@3Kpc_ zo@HM&HGg6x3&b|49W4RGTY|2bs9yYO$mr9FDgPHai210mzcIBaJV_(D1w)$8w5Ahe zIuB%s;NGIr%qfQ%k~AK&d~YrpeI&*IZ%lLjG9#1!Nnk96wYa`ML8np!&!?&!BuP-q z=B>RYG%Gb2aL^+oBR8PJD@}HLi@uMGKz%?4CGgp(13V&5u*ch3(8%j21;?PUKY_J>GE+0G5YFBB6WPm7po#Jes;e5 zn0(&1(QW%hb$rdoqb4zy^}$Z|AGY)6dhYG1Oe_F+#FU3 z+DwK@x#-W)ehn;6wb^>s^tizS{p7FU7yi18A8A4! zf)p?ISH{?@I}^ISObC#by8uLLzh(Ivw6~1gw64gJ;hmgmy^~uzX?jM617D^ZqdJ#@l$exlI2)bZaZZlZ*d7BO)2{%I ztjdI0xhmuAd$9zEhdU!_)SaC+mXCp5yJ#Izqqs#6G(YgPR0e*mpS~^4D$=Pg$VZ>0 z!DsmRd45R)|4&fYqv0{6AyA7s7;;*y$HI!5chuVXQF{f~P6*woJif^jc8vjN6WV1^ z=}om`{a{}6_CH_UvOkTUH*#x3##U%h*AGv*>kVA_a0b;WcjH{1T^F$0YrCo)JP8=T zzZ{KX;M0tH)}vLbz3i0-@+;e6_#U`lZ8VbMY#MKbgjZ#~>#CXb4UR@3Z(^8t&r;>` zR}h5^LJcd?*>*;|;pU)v#tcUr$>LD`6zY%EQuPAwUx&(*{t?OXB+QwVB7eiyK^=H* zI}nZw&?M+bfK~V*N^x;72+n8x_iyZ&A+(Ub_9OjMW6%vTFflp#`K=gXTFisdcm^9i zAy3i3%l79IiiRA>#mhUpx*9oqAU?|lu>HLCyy%+(IPCc{>jkA32*T(C2#1-HuA02l zmT$@9#&+xlvf#!iC*vn*&)tx2y*xeJHqb(qR9RAD+l5sGh-bLNmxGG8^U6;spbTT%kXJY#8WV^| zaz&BrCwt=abP<&M6?HN4Yfsj`0Wr{feWx3EE zMA}rGal=xhc!UDWIh0>m`<+v_*(Pa=s57Zv6kMvx!e(o1ztKlZ-T7rzvB&CH8mYcF zo{8ZSfb#O6b#C(M%2{7G08&rX;t@@p^qVNjIJ304IIL&pV0)X|wkgB&Z%fFt*XnSN zK~hq3zkKIGS4U^5#R-qhd&A^-=^Q~Z#IF4Yy*sA0T#6BoO7`EZ@F#{+3h!otEk z>@XyztgB$-d~4z2lKgGOwy=d?vQPzF zN|n;Y(&RC2Qh3@a8B6IlR$Y75?xKg3zR0x?GNJaq zpKx0qafPw|x{ev&%cCjoerc^Jpc)mj!#HsBy^zTMH<$umzkXwORsR2yx=1o#To)h& z8&^vmn@BwB62ZMsj%o5emYuKqb)t@-;g^$yuylC7a%%#EwXi{#hshvr(GTAA7~FKf^;T4eh(H{8ClvB*Us^m>0(td(Mc3?cOdzb(S}5 zdR>iqMIO&x9R#xOyo6_U9a{9jZ~qSpwX(LROc+Up3Of8jYI8X;wzNcTTA2V-nWE28 zlTU;=WzR?gf|&i-`xVP?-*yTOAH!b=->_$UTrh}kvKhqC3$b9Ohsfgl47yE%F3jX5i7~T!Kv>g)r)d0DJG7N@oJ1=vj_c0 z*KzW+fG~A?-&Ki)9wdH{RFV6eX1Hy2`~^*X6~}-vL5PTKMFVooqMR!XT>!KVkt9!g zgHNMm;(@RHc6`|LLaa%)XVm%B{XSE*GetbW9IJoV3B_EVK|07JK3iH@v47sbcDtRV zUGQq-Mwlwu22!N25V9SvXX<)u=UP3_LVY7sW7Bqr$^d>_k(GJt_T5uQh4&Tb)#DH*7u|)T4Z?&*xCiDx?z<+dPxQcs%wLyuPsY5+-X}b0G>uM2u8aJg=wA zHgf!$n4Fx9{I;xblN<+q)1EJMXbTh~)lxM+a5h~JJFNVU+o+%xH+4OpJUpGkgu(os zmq!7hearQNuGP11Cb!u_f>%?J%k2autSScI(jp`PNJ`bv!2_qqDQaj4a%>sDat^ar zZ}=6h4*dDx6#y-&?e=(J=bSA^-k&8wdVJGa;F(yM$`b=t-~4I%IBA@m$w@G%ox5eO zuLB2ja@-`|h^fkOc;rtjKI*gp=1o)-%LoaF4>%~Am4&CBm=~u5uamX@cuz}+=0YO~ z#YwOi@A4+7_6^4fBXPgbV=k9P>kf{?xIcW^8n^AdoE9lirk|RfHQAjyV(|6!Sub1N z99Qcx9_=K>>sApuF7&tv)>^EMo{j#m$=1%$_PCz(+Kx&-Aj_fV^Jwn+y$+LgBm`U~ z@H8ws&n0|r|3WO5>Sws_48n%v-Dt}MGKUZ3Q>c@nxhsfYB;2CkD~6fyrp-!C+OnhI|zR;VYg+9#k|=-$M9%fVTn zj`+0`Ftj*8yl+9faQoBqV!7+oETD&+G-fZ7@W8ls7(A>(l`zq~j~r>+VEh3%^G(En z+t7#gp67c7(lm66na{51la;1tx7&IL>m|@bFAB&WQFA6^ii;kU_5!{;wg}eO*WW$D z>*(t4|NV<&RN@xk*b-3miIQDPI0pn75~6=Af|vz?0Dl1UNb?%YN7|g{mxcb_%bzL- z>BS|B7!ew@baZLiF5o|rEL9`Vm*=U`RGi~6Ft{iT)Ky2G10tLkVY>AUT3lCArYr$i9-0B;?h{e$j6 z_F~iCpUR`94j(F*9sW0sjT{nABa4$j&KBf~QZ$>ttJb-M(3)LHB}$Y-v_=4V`R6Hm z8fB%;mBnvr5=Oil7`4%maJic3krHKlK4MUtcFngiU{H~oCtsgOw}1qSN4REnu|a?6-lemv%A2y;pP8@zm3+tMc?X#`gBHy0$GrfXTW!UJhg) z`_cF1W9%J%iwOp^>UZDE6tQ+WV%4g%F1eU zLD#OZvNFmWdV3ic2ewMiyvoPKR@-woM6OR&<)+4*Vg?3CN4l zOy-CI4kxEe@q&q84UJn4o~c<`mGDby)M&=F8Yn1LoBPuy)CnVm-Lh2p7|)p^#-u;M zFFCY>9Th4&K$#?qED?9Ge+*uw!07Lbd609KXe6C;OI#kPv%vyx5hKU%F(c8879WK< zH_U%Zfe+}6nathx20x%eJX}q3GvFRx&WJpUv|shU77!2sXxJigm>Z#YUf0&pTXJvG}H&qCaK1?-6ILb%IaCB z90M0SV_}ljJ4>}&KW7Q3Whk?L=a(@~OiTc4{>J4!J{cKZ%2)n9|#u>Q0deFsnn+$ZR%3*y$-I($q&p&xc@g97W46#>_9?i#gHq=ynn$sVK; zVYPNP+Y{O7OZTjpwVQbpnfW^bj7B8qk@q;$xEInI7LCl2<65t%@g2Lv+WSWWqALtN zvhT}BVXFf)VMi{MtbYxM;k^yQ0<}r@c1TyvVl_M9H{v&c8^x`fxK6X*>?S&2) z({4FLs!;8mwugqKN@JnP%C%kCpyyI zA1ROA!uSd9Oky*+MqA2XikyFL0!L>gwY2Bxr;I$ zI7-E4{dD8~1hQQ^$Q|#hE$`#wBPhgN3Y=*VAAd;HFEbAq$ElQ>GLFj@8dqh=77m+o zxOEudNcf$i2O*S|Jqumyx1G1O;N(EWfFGP3u-SfvbY=nk7aSaNpMkkLdFy+I2!1qp zrdPMOU>=h%RWo*U#FmNwjDz^1W=af+K52A(0>mgE@X4pw))c^49n>3|Mx{_#S5l@} zs#c^@np>vsO!Cr(0t|HZfY$uO$@$FSl*PTHNsyT?@7~O9E03u9&2w`K83G*`uC${| z^fL|$3JR*n+Li0~8jovhK&ADL89q?H=(?EvdN%2UYgi6gu1sPFDZvte}ie;|*zWo0w-^FhpGyY9S{3$>mDH#HtYtX*+4 z)kBE(ouj|vzNpywUc9zH`PjUiE>!M-YMcu!p zO#AgX65pN_JiZ1UnqS;A3LZg;2(FsrO)h1R3r$Z?42(fZ_=#RYK>;xITaGOtt7);V z8HF~y|Iyt3fr!G##Jle!7;w$Ea=2|4>;Zy#J~I%Ld_J?aHT_kuOPwS1_tCm&H^@CFLp3u=`s&0 zb#Q$;VmtHhh~i*#FQL4>ZVl_SPWn-O|A*EKz;BKp;_PBUA_j8G+TBo zxWrR3Nh^QGX*XcqqAE`HK3w^Fmu0h*vx)?=QxhYGg+xv~+{?qF%NoPKEV|78=VMIOqGxq)ofePj& z@_c&WSpnJWqu)IoXQJJYyC}hyzD+jVF7|tT46lxkj$aK84&V$xd=u_yv3jrL)SESf z*A9@&&jDP}#+&bVzcK4Bi~$MqeqTytu!+nYN5TP2;-DcJ?TjW#$M=tqbDg^pYl8_v zqUQ@en#7xVxz_|?goLd+11I*I%EaRn6WUW7jHPO=@BV6@-3!u(M~>~#;$!3<3B$#Y zP?wdZ?^*w%<5^n3k;qeH=E#=e6cJHURZUnvQH~eS2|Pb{0CT!j3@P^t8D4}0kn`{f z3I9kL-XnAod^0&a@tugvclFTfuG zD%RN)#5Yrl^j=`Ihz(40fG+{=zv!wXViBKA;DnBoHIp~#&qK8{j5ni}kc>Aol|j+Z z*QtDifrJFd_#7dp8sh*&iahPW@G$DVSDZaxWR=vp+v;##pkvFA$-RQYLS&hE=|UCI zR6(XcEjybhib8*OWQXHteExy5uI>tghjt(S%=avL+WK6C-m}5~R#phe$o5+7ywlnY zVQrD_FNW=X>Jxsz-S4Pdy2yMj3DV<6a&+~T)H?4Z%$qc3v=Kt1?Ye?%%k~c5-8B#B z*&sOaCfXT1dbGVwJZX4}5`G=B6&czS6z{R#KvJTq6vrd$#JuA+$WHjUj+(nrpflM8lk+%^>U5X8^sUU zVN}Cp;&b1g0AcHy!3~zYbM^9S5B96hFSD-nG#JUO85$e}9c|TVQphr0@;TJF?p7dk zzw+OIqaaSVe{^JG?UDWhc0Y*+d~4 ze@M5gJD4Y*IeTeWrJh5Xp@P#fN z6}~tmK)h}&cO|AO{Njh8u0F7J4r12)cfFhyPXh(NY~Ar@cOp)E36c=t31)D!@b1*|VA(sk!NAEj;eZy){dFfmDC)tRrdUiM7^7do3CYL`` zDPAcIk*o7G<&(kp*f^e$(KxBD?(Nbq#|r?%I3>Tw}HZx^LT7=?$P13fk?Ii<}x!cugtJ8wBAj&XWZY_l`}HJunyxT z+xV4M0Qr9}z?k7v-SWH=`EAOUJs+RrHa(qE_D{X4xe?3=;<}@PZOp(o+)ALQ%y7Y%nv>yane7)=nUW(=;JfKRJz(Nv$L8-CPgztKw_lKHY z!IMXY4Gl>j$^FX80ND>JV5u6R{-O;rl18<%XcYm%j&WRL zeQWd{Rv&G`@bIt@>W9m22hpTDg^Gq|z1R>X9cpDga;WH9kx*ApsNU-L@2Gf)0nrjq zO#3>wg9^ATLV8@j?0Fe=>Z(^SZNvTHnsLJrt+Mh5<6HKK7-8jKU@sYMkbM{k=F zp-gXzfZ;7n79^T&TpP*EH}^5$v3sNlPX|V9b}L$XU5@?E(0BFc`cI6IaFw(qoaT)dK%|^W2Rd=)6%;aTDeWUq99k zk}oYRE7+gBn;|93(5v~-z(ti?5{~DnubkgUC0NwLajXy~{CDuiBI9Eg$0~)w*+cQE zg|$~CaE>E$bw#zCAHncei6VNdEijf|`OkI`9#-24B?~3mnRVD~pDe2WbEyrPK2k^2 zE5Ye*3H9!Zz(OO~s=36|O3|%_12;fO2lA{z~=yYcng)8m=MsxUCR-4n$t>@`s> zic8E@_n&BKy|cE}!W>&f0WQ|hu#9q~@B=Gm`ul&BR(#xEiWs0wFG|bqDzX9(>n)7| zJ&so6R|1fBCu@TEeBcCgR8$mJlq7&BF9QPu0s1@d4t%B~VX4y8IuV4Shwz9MFR;V^ zu-%?G^2eDCTxG^lGnUE9arGYAwFR}Edc0LkiIArSKGZuCl{BvOBFE!9v|z<-?6=0> zINte5Y%WU{eCBHC?eu2l{Ez{Ho>(zMHj;|S+8S_Z4Jwk!)H*sk<27Bg2h`vKf?)&D zEehmDJQ-kY|8wmz(O40JNk$7q3bQ0oX}|Kw)i8{{x_ux}N;Y<8z&1eU2~s42w?)J< zgKn2kZR1eDYA&zjT0{y|=X+RLvyYlazx~DO%Xxg$mgAeAocylcpgPQgEz!Qi%Tvgh z>1o5)MfzVCDNn9J>t>SMyVKE@f@YIAo+K!*!)xT9Nm{GxCU80qd4skFU@ZRr6MLL%8qP*)_kQZUS-#- zH5XOD;^2I1-jV@N$Z>cdB{PGnNtEqdtA&Jw=eKxNsD|Be3Fho)Kh~)_czZL@~Xl*CxO%LY)XYB7>R?aGPd;`e%iXG`6)Ic9k|ET8_T3WH-y=JFKCKXGz# z^-oR37FPRNW21FE{cVc-@5=n9kj9)b)nL4+h7Ex>{9gbqqX0tG$_XxM41@xeKlqXo zQY|evc1u*JA(P;Jm2-E{)D=FtM}-}hI@z-FzDIJpqc$`#uEF9>yx5JL z$hDL;!*GQCC>)pE+9}iig0t}7>8G62vOV8Y3CRt3#htu?S3VNr@VleMJ=xWg_YLfD z#Ml@dldoo9xSzkoJYSs^0g3kiZJ2m$y;h4B=_^GXN=wKD6yv#w%!5fj7?5vZPOYs)^=)zq&5P#w z6%o)k&r%qHnGO{xR6L*iv`v;H4UoYO6WwrYYeI0Dg6`pT*>3U90%D!9LkErlEEG0` z=~a0PvN+r)Tz3AUmiu=P@LUQeEO8%axqw+bD~km9$2j@fgY)^$ppV9;+~eYW#I!i* zc~*pA3m9*{HU%Q zVv)?{8CUknF06l$*7o&(F6@bEpBzo}Z7271sc^4bVFZv9iVKew{5_MC*Oo%j+-AmI zir0>pQDxYN>Kt>NUdJnhTeT@~b*SJ58#Nqr^;m}F4BcXJeb3qI9rp;fT5$5I1joWz zFqX?$*f;Pfc8~hbRIuaov2oGp%LFQI!AvnmjuLFrna`<>BX07IG!&P5m+>Fd(LXoj zvFafBjW{lYy|Dt{Qzx%~Ij}fqMhP(Pre$PjWS9W>G6Z{kY3jr;+HlhmCXOHze|GN$ z_~?YIAyg=se>tV>a+jk7-GK2Gh&IoYVN?6gdjM_#?{W@(Ty>sR0Ab(%GL=7k5V#8z zx%2M4pRnWL7Mkb|hfjWg)U&DlEDI38{|gu~TvT_~r@}!8Yp85vCgOS^!DS>~YL}7UC73|iU4tz`)KjKl? zn~ZSV%N|%`q(kq`sWHFVq6*}#Fb(?~RDOng@lk!NE>TGB`ad}5Sm(IkRn|rWC(_;A zg`evn|BzxHk1RkTtk7u~#n4rfmp?n}!`yNf0GOgEAcY!D+d-Lm8-$0N*K9@e-XzHm zs6E&d9@r-SQ!fX(1?p#>`nd{!U(`3(6zPIYf;Bk=#Kc^R%b~1J7mfhin8Hv-z-7~!4&<$^)Dq3dYZehi6i$v z67YJ-7S05q@S{lfHrtX%#VT|nQEUT(1(beH1GT&X z1{~!g1Wy$}V1M~o{@?sBSRpn+Did1xhWe*3sNz}J=XIUihf(!1>Tg>W%XXgB#b5>~ zFZJ*OA;0?k-%ewcSvg(bDz8Wnhzs)zlE!F{s?+1h|9BzE`|#e0-K3qrkVGV(60ww(o!fYNJt2L2Ugj+nKQouzRVZZQ z0p|_cGG!dSVsW7K&oHCzvAH?w7nr2GQ+ZSXI+vwkgd^dq7wWu2lE6m5a80kQv6ke( zF#CcJfA8i0v3pV%;>dfRM&NS1JJWmXloO8 zcSgtGa-kUPblqoWtIf5-m*~&^Xm{$d69#N-h|g=E9H?6X zk;BH_T7{oquPIYU*(2P*RxV-Jkv}}T2%!E!qpQu07GldQRHzXQ2a@g{P;SeswN{^k zN7h2G+V#5>LT$FEg%6rGghQ>-=RP@v0-rYThnoNLKH)*@Q$(VVH2OJxYHmpRXxQw% zTVK{HYjV8v_!Y z3yJuKRd+t(qRNJ-+H(iTsKPj1(G86~fzaDH4rslu2fZ`$<*4!^~KFX3v?S`Gi>C=g>BU%w9O zdx&`?~h zo;-o9T?3i0RGCFAxN@xRW5Kn&-yhkNgwR4?fz6}GW32O~$i{#k2@ z^QQtfM}<9xcF65ua^Lh@zkOS$DUC6img7sa*@p?yw;nPJ*qAX4RbP0iIidoCx-QM;_ zj~EdDV)a-srNK>X7tT4*FK-1_HLgu%)Z)IdnT7C`6B=S$(Kr%QPJVL>q3uk?2mfc{ z9k@UMo50BlsBZ1L2~j74B`!^oeVra+>@oG-ILj~yh9fEUA6ih58gt5awJVZE~=h+ z2cxs$H@JQ;&>8W5aGt@c;kMiHKY?Cy)DIFr;U2B`VUD4BDbD@gj5Yz9{ZtWZJ51kf zi=+{@;xvO6A=`r9<8tp``%!T_hReN|TWx_b3NDbMzfRWRvgz4_%z%XyXI+3Ibl z+3rAP!dD9mx%!3J+2-?H6gLVG?H@=F+gVH0|(0%DVhm)_45;9Fi?+=E7;cN9RC-mbOrlj<=O$Oixb- z0{!5QBO5z=&%*;T@H2wFO4t}E;Cg=L(LFL$9^7)QwVkjQ5cqEyY$Z#R9~_73wqOyu zKNQ8OT>gvd0__YG8CBJ{x58%(w7i{B-cBsZ7kBT}J{!MvLbK7CRSL8r#+JwXZZ6x4 zsu!+s9J}FS6<`PJdjafTS65f$W5sDthd~WQI~ykyJ~s634C}rfWpL#))x{;hr4GN@ z#lS5km7lNK?$5WqyM~J&gyG34WXfql!uP9R1aS37U)d!huqA7s!w>S1#)JSCV3G{R zlcfvkzdLM-t#Vuzmui1e`F$PnP4uMmXF3DyETrl|c$OSBE=w1=e!bpLLUBO>v{$oC z|5dZ3XEZebz;ffG`j@U3jg*AAp`}RYNnj8j&SGu0vWlIp6LXR>D**qZf)p~)X_3>t zcZu~n1m4oxI=`i*1;w@;JXkO>aHKs(@^q3bpMd&_*Llyj$P5|X)Qd!u;#_$;v)dP} zNg3HR0R5~28;W4jQfIL0h95?p9ogqB>;p`vmVR8f628DeL%YM1jpQz z_E(EJ=Vh_eRPY6Fl~th%MaF~*JRG9B#%W5u6fdU| zbS@uA8e){N%=kj;1)gZGw%VyuO@{V_d-GCmr&d?>+FT0jv$vMp)k-Gs7WYkPV6{~? zc5uHEw+xtDGKV_VjALl=eT7Idox+eNy|3#l9yb(@-}9(;%Ilr-s~1napVsum94%93 zkv$<%eslUMzv3KIcv;o+{a;)pnOxV*Sbh*We4#3f(B3c{?ezAL^{38KSxdCimsN~(eXqKi? zRU2aZ)vtpVA}T+$Wy+zALoGXi?R1Hj&^`=YKr+}tqIpG;<+S&|udcXQQX)Q*8>u{m zmvjsE7=>0iNH^qC;5C~J+P19)5Df)@6R0}H~sp4xl2MP+}dMn#*LW>O@C+g zE42L^S>{`r@uZ!XGgRH$ax~HnTlmpag~?->5#Pv{_12Lb2w2@|6E^2cRl$B)2e2p` zST?{;PwwEh(EvQiFzlf9_xY)B(9tgjxLrn$+HYMlL*Yr=jx9QPLUe%O0a-;k)(&yX zxZL{sON<>@?@Ly&6dE;1l#e)?Ca*!R5Zm7`C&RY=`FJIVhGMC+vj4K^7_$?BkNsR!HQx~8hF07 zC$BWx?Ya8iy0+}5+uV&?2=@U$-uw6O%?ktd;R)9}_3w!ws--xf#w6J+T>OrK5q)TD za$;`!alt0uTt9-Ak^bX?)ux8EkW(Lu*vS_BM5D7*O^{Det?=Z2ZGSLeFF%AXV5ZMkOp-lIzN;BH_eLz`-X!V2 zy2hUG^tSN3jv`fRHoW{-TBV&}F-5zZC)bApI#sOF>a}b<* zOBWnVlcw9WCaMH8z4iI&r>Cr(`n>kxl!${QzDTBD?l5 z$o-I%TipRlu%ks+aXd@!gMqfZ2TI3j&#yu8JdHg=-8HJ0Oi&D}ZkpJ-g+~#1$sTj$Uts`10(&H{wc z9g1hHCr1(^l>gZ-^15-JYl8ZpfcrLi3z>HuOR>wyZj&DFe|kJSIdiEj;o0zW8rO_$ z+Ee_g3scUY!a5k3w<-(Af68&Km*>oqxxUKa(VvSmvvhsguA}Z91Z^BO6YOG2Myt6E z*5!YimEy0pYbworRXtaBRT&EL$oo&3zJGO<7}^B`1T73jNuc_R#>G|?`T!`YXh;ad zb&-%$sadYqq=Lg+F(QgU<)HX$QKC^Uckxik@61P%Kgab7=;eb^M8GFYEaZ{ca_dT&;I*6Pa?r%ETfJjaL9iV)i+lad7&TxgKSLDj!d`vXH5q{<+NgQB6pt=j9$bR%DTX@%0Rc?Rl?^`We0ZUz@k@ z!c})rvd7}UQwFun?s-&b@U>v%j*H93EmxD*?oYK+&5yr52@-T9S{5a6W-sk@N*;>C zGvkEcZW@Kf3Etsk`RBTUFAdT7@K>uYC@w}CCdpB8^n$Vn3iT3=?7WiccVf-24_*iP zUi)HX|J{_nJtSgzK`$w`EhNx^fW)W!j+#J`KyTt?bq+FkPND>&Mhew)p$J+@z8z@) zrDnbw>^=x{K+uUBE;^APrf97zAoksTVa}o-Po3~)c%v;RHI+O~o>B%oP8x{z4J8-6 zIF@p9FM;z7=u;UR`~{?I2hKAfkwh&cub`AMRHDJeN32Nv8d-gwIlDQx_8KYy-S}VD z5DcE6wgS~_)dlJz+5=~@WfB=3Rci+kR?Nb_3zMccCVVd$p<`09Kp(^nGb~k;rcD6m zl&Y1wBwgVP8lO!9(<0{^PezLFEOmW(y)|KfXVTvX#Ig~qY0UFwB42OUs!8l<4(>#mJrQPE^U_y3XsJ&VoH+#~nSsCJ9CX^RK$zn($`HF1mjc7`{n5$|od6rnsfN9N4+Ot*!h z{?pRG2esGM&;xnn+e1$R@aTO*B<@qJUsdxtJ!P;iGQ!ImlSVdEBfLgS!AOwEz4@z` zBu5vncBFQQy(aseZLim9I5|ya8u`R2LN~vp$MiEV{B(*=l@`(N$KO3?|23U`xb=;< z=aUEWQO;yW7twkJJ3NtE1xjHxXsvW7z<)M-U^?XMl@#3(fB_j_d z@F{_&m4VjlqotZJB)K4troHthHkNvs*?sPNPqQX7`V2|m5tdna{#yvxq%F&b13om6 z?k5R`IvX3RcM)l%aSq=iW#Pmsvb*9?*x-D1?R*hegfGmpT`)OR4gQ-gJy8Dq?5kL$ zYa|#uB9;%oNUL{%Ld`&_lKz+%PJpIGHmA+mk}&ji?gKX)s-oWT#mAdLWfHZ92r(*h zOft^DKIrG9`fpe0CGwVj+2ea!xrtL&^~hSHgM2#Jbppn_!Qbe{LWWrHz29g1UjN$b z!f#fWYNt{}drkX&@WObtLpV$wJ}lhB9d4!CERNjcse-!gHNw8e*tV@Q{C~ddeJ8T zR##US)J@<9$jCc%YytT`Qwf+OaX6xLSnWCcM@qt|Kj%mz=1$;3FyempvE*XocykXB z;;W;@%}X~vk1JFXk}M!%sNU;}<%Q?JK@DB#lWd{R%*LsF6$ES~z*aK9Xlr~4dqnfe zXQfij`P@{sT^=CdL{6Ru7lwVTbU54~qhKg%J6_%G4DWJ8N%F?AoA40_^0;mI?1uxv zL3baQA42wnw}nqAFPW$#VJoxn7MB+%C<85SrBV6Fa?5}b|4w$J*Et{2h=6p0g~O-N zX|h&?#ge=JMC4uF6!j>0SSWS8*s$)tD93<*8VI_(hhyf$tf9r(Ul9|fn_hEKC-rHW z-@_1A<)_z*lMS!d;q~!O!)e1@hpMHy+WJ2vRfcn=!@?ETI|T+9KDC!5+{lRQO&$2B z9#E0yi!x z5v(~kGt)bo#B^|Y$U%jt5oEgU`!WatOxb2*nr90!UfG5}c;f_K>=@1btzjK`YI4zEk6AJXR!(ych?fb0PJonPuk!8AQEast{1 zYFr5279nuo^_dxhw}|q zvx!pBK&I(!aO~RB{$z3JiA+uV>BZO7r5;sGSc26!zVl>MVyOBTW?C1CZ@dWqN7Gq` zRrP&co9-^@k_PGS{?R2M-QC^YA&4~62rAtm4N{Va5CM@EknTFfd6)n9dOq-(>+H4m zT6@kp#y!a45sr&Od)_T@ch58t?Kn#G4gZv)`S>S<_(^BPH8lKCf$4}fDQL9H{>orc zpo|-3bTs*0Sl-{G**7&x5TU(r!CCxMt!{=jR@3+fxo+pGu)tqm`}BwDWIomK8PC1P z!)lX^O*T{Z;@L?Wfm^aQpB|gEQ`Vm^-*I=` zxTV@;KXd;@OAg>>ncaCm<13uENH%>Cu!C~zg?_urI_Nql3<-mVfp-$PYZbB&FCQta z-r!t!8&sDy?2Xh~S|5717%cUzwcS{g-+nEU70)MVJsB-htYW@-?D7qLI6qvNGrsk1 z3xUO^T?)t^1;V_k@;N5J^qFaNR7w1Sbg|c!ICyT)zU7t3o_mgWF07ju%icExWggL{q(aF6wBp`s;}^%c}<6dSX|eA-8-t zeW!Flhm+!QL>C2Jj)a+uU^V)(KLGpo=CXZpg$XCXSt&DR02;%;(f0g)jj^JGni=TT zIPi~8)vqK??AV;Hg@P}FGX3gqw?h(X@YXZ~+X?Vef`19FcsN35un53%1k~5RuO8#$ z;y@qIPPv@NO;1mmfdKRv)~?J!$PmT)pz3mk5M2&lV52^64Vn@Q!O(HO#GwG?rvqI&p2D0yr(fTb<;pr~?Vkta&X zT8Vq>|CW1+yWHDd^j-@x-&WU56gN3Mj(kYvUwO!nz;CoDG-4lNW&XJP;_i5=e$OIc zLgW_htCk3?Igvh+esHrz<2(8*&O~DPL>mSEnNF1#A&x_@22Bi^4I>5L$#=c@lvGxJ zxyV%D_THQw5l$Y5sdX1e-|g4r+FmgeB@Q7*teQ=i9u{(46&vXI0)-RMx>COzeWXZ~ z2bcJ6Cz#?((>Bnyn6nT~>dF9-9}f~}=KVWtfL9fOr`{_KU1_CoTZ6+NG*p;8TpJVD zwWdMqn?lH4adGo(^QDiU&ySI8OVEr31vf7*uT|>urbn(?BCsle)CJ%o2kr5^JTgE6 z11F6QuQG2Qe@Y%W4*@@Qczofi=P+7a8;zr5===AF2OR3^_=drhN%e^R}}A6KFDW-=7j>wsgk zR)MWXU6CBLVV!sGA?7MV{S~@+9DWit+6ae%OYQ!mg=#79JdfhdcYfWPK9S8oScx~SHa;!=Q-iSz}lCG8XubxRaM0lw6=ZzKMfB3)peqmBv9}+J%C;Q&)cSz z@Exx3#7Slv!&?iEK<6 z*v%V`*qe&KrFJI`fM9XIvkc|tj^5;n>@*4%@AHbUfwN5`4Ne|$LZ4oIii6Rc$sj@+wd@bUMj+( zpo0(8w>=gEhlS|9m+N8CM!u;aB7rVuyhV83^SUAcw*U3nDk;2TJ2H!B|2k~imu6pU zcp~)CEWP6KjThxH>0K|RsD6FqVf~p7Bf9=$2NKanfo1sXY!R69k4v6a9mp?Vxr5~u zQ+#oeSLhww!#2;Yi7z)?2>r5&l)X(gD#W-9dks7t7>3VN}(DQjXXZ{kBo?c7-18dj9 zURffV&o!WtiR&k70NGGu35gQZN<%FJ=@e$F0MSLq&no`9;RfW=nEwtG%&zNjbtJ2ZT{vZtu@t4r1sYE1LKtP2JU^|(7 zfg}^3ZjkghDHO;|TZ~?v^gg)#`{Pi(W(6gp(j8{ep0Cfzym&0AzQOfgvsdMMHQ+SzBSeSd2n98(=RX%OGQknfRH07Kq;mN>LerK>_P)d**u5F#nawXx`_3} zyNBFxfi|-)TNcA>I_z_M)&q?ks7CrP$peZ z9YuO$?s>*@54Ew7GHf@57DZJg?p}mSRB3&0_?gBC#O?W-0;Kgl>Z#4*(#Oo>lfAIg zEUC%%)X-A4yz2$Nj%rQrg4d8;@_89&4SupS9}}{Mt*Z1X=SHUf4=pbk4`evZE)jn} z4$^GgK%N8u%LY_YJ}#fJ;k=vtDVgaq@(!;UN=n11PTgCLlv|~1U0P>L?vei&!CAC- z4klDO_0?=)|C|iEY;;2*gc)w<8mzWuj}$2=fN#w8?@tDg0ZDk3c-D7ysB5tsfe#BW zjt<1?IJF7`$RC)9D`OSgHIIQEnRC*cL?QKD{Pm-NS}++-mPTMlH~rt_%cooYm%nB^ z_8y!&^iwZ4!k+}H7TAEORO5gp_x99;@#&!@ia$$1|46EOA7Yi%mS0Y+`+;F;VqywI zAos8n|4cs$F1M%|Z&f)gNXl@%xc)4zL+Y}~ZyTLM!N(7>)Ho=9E)+hb*LPt^?SAv> z`}v91vj)l$j5ksK8uM(+{xps*S@J)LjCbnFQ38Jx>jXSs<(@OPN|Tu#dUNkq1~+GO zJ{@epMRYo;zKutEe=rro#&&){1EuwU+OAd8&;V(ZUrRv18}%3B9=jg!a+V7ML>^T` zU*$&Y)*`ES5Klfnd?Nmgiy#>Vh3Yq4fIU&^#P-V<_VxP*N}aOHK&1&udpPZ*0l2ol zFbSRUa@WS`+)*iOhE=fbe(nhm)Hr)SCs`h3n!-}mZX4HRH>0(U!fT{8=T5& ze#EaeWft{HRX@s`*9f+rshB6D9>Ube{u6)HD+=D?K|w{$5I^RCa6YYdqwe~zN)B$J zNVmsH8OryYp-BJoK4x@8?lc)>c3v?nYP?_dlEYC&hc8cw z(&n19$mG(99DT}?&FYJOxw?dWLFH~~GE@nLb?f)g5O^E)XY~;D6|bu3?BHSb?weJb zjcKlsIFQ#?p|c->>Z_7@^ViqD8{F`vT1uG}rw@tgKQe%Zb&649_5)$-F20(k=H({K3SNbs-%)ry%HU zJ#6?CgdhEv(`Tk4?i^Zw-16ClErMUI0Qvm0*PWxon%$XgL^HPG8JKAet%% zd~3D($^Qtfz=F80+yv*d*W`ndWq8GW+;fmvyWqsQA0~?i%5`v7n|KeZo7}uN-!5`; zG+dN;`|9TeTa=eLI`x;T&Lwtrsm`^$t!KzH_4en{r4~oRY+GM`N8Snw)t(%oUsH%D ziboT&aFcZzj)*1sv!0S0C*T(EA@XQH<{RynFSitF(v8`aO%C&WHDR^BjeGv4yeKg2zW^KZ-&GQ_t~rP0-C>g6bPH6T-oOUI2dc^b ze(9~@e8SITVoDzggMNc&srXT*d0FiazG#m}L}UzDn1o<+r)Ns%804y&8gN8c;mX<# zrL+n3!a5mhZA4fd>6M|Xl2*_lgPQ9C{&9 zx9Xe`9fkKRS6^God)CaFmU=s7ZdbF-h6c<=O^G^8q6o~#?>QCb6hok-pCYZp`Z6!4 z$Um;dPm2a_-avTSx&wFWCmuA&8enXAR2aD~mG-C;Tu`yW^n}`-%M@kTG2c6*k*Lm=s5V{SoL}|4nMaV#wi2 z41o!gLaULw&br#pdJ3tXY{@TL?hZ^iuuXR-$-Z&_S%sY}iZt9mn0S9jy@zx~6`n5kyvvoQ!^_X#QZoB4%w6O6F2nki()SO4yLb-g z59e-Y<==EZNlNT;?6l-^{1@marzo0QjSo*(LZJe`bRI62PN0fr(=9K{&^Npk&xoWg z9>=}i9gI)bToQ2UzwdoGLdGozzgj0J^l=>I2vGmc{u%1al20}{=DqMDQ8Hogv^*3ipFyowc>~Z;-L{J8-WM_~w}Y2xTx^ zq|)`|DyooE!42O@dGBKBsDxcv!zfyX!ZLeWcaV=>5Me3$Ruq`1TVb;5r~k9`w;^Whg-dxZ(>A zJ+B0g>M+YgxQ@xa`+jFKp>t<>jIU(a(Mzs8RKnI_k{JA}`M#l1bj zz<+GS(NT5~o2>`+pU#&47Qz14e}N`={7rb!VpQ}LrF+1NCf`M@rZ@D;5k!d+2rtKA zwJy}ZhF%TTNrYcEXPc$fR8sP{6fbZCeF2Er2J_DyP;nJVhJH}|IM8T4>L8e3W8BWE z9L%R2Wz@vPKL35TKB!B-dKln+j{WlepYAjppI`QkwcCwPbgm)n+`lEUa8@>rCM>e) z?(mA5y|Eg)>w6Sf@42J^-3Czc5)QJf-uqr&bfrqflKQNCzF)O!eQr*I<&H+LdHDYH zH~b6^R{A5@$k^R3t> zXwgIukjf3WR*5`1)86ijIN6puWBy%g&P)d}EPzN13egN^O`U314l^NI-u$-ml!%%{ z4GKQT>1fW&6}R@lL9zAG8}mE4ld9~T)xQjxPCDz_1b;YkFNwDeH1GCAORq%E*iLEN zLw6yL?JFP{j8WtrM}X9;Jgv4&Od@Zgm-N_sw--=#IZkKuWHS1R_ihbstcE;pg)%6# zuP`~1``a8>1YP@c2!6S`Kt9$l54pCo1W{MmWmt8o<9vtLAeALzt)Nn9iR{e$5fxFD z8K5(lmM&sJo|{F$MeU6yESvTM;mtUb(GPq27%inef^P_hi)C*dEF04z1K&;B|KwP7 zb*oP4`52Gud3KIEX4>Vg#~%I)+99r+Q8R%KHN8I45ac32Fjw)^?)*ai<&)Z=$aUbW zjYH=+L_B5WcB~Gz6W%@5D%VO3v!N9l*WIPNPsQ7xBycbK7u`PUR06neLrG@M&CS%9 zw94`8|Lii;_J#$1ovpR@g74gt7!sd;wC92W5Qw z)8BXA`!FbTNSA0#yV_i_^l+=+4Z0#G+aAn8UB_9@QA7mB+K6Og?r`>}m>lVH^| zq4w=VnE2*T`KwKxRN>DDb(AVSwsYP;fmiW#p|V=<)0^VfZddtEJ2?p}PwdjKz%($-WhNSn^e~`nwg@GWBy{}!}$gOQb`sN}fMXRoidi5WV zOyuFsFdC6{SJvG*q9#Ib?D&vaH!xle_(cR+e;2a zex|l|ap4%d7OD;X8#B?Y({Y7VN*w-+a35vQt|G&c2<|U8eGDEIW9a<03SS{GYxBz+ z)soE5&;K~|1t-zn1{aLv#yISQ*e*1?_E9G5D~G6uvhoHRse7dnjQu#E^&_cv;8n8t z?Eut7*|;;f5vT( zyvU#rr8 zd~0X;Lgzw|@|J-jsez8`YdHt0JKI`nGfMYdE0Z#5335IL8`jL8^g>QGwXY!`N(7fB z%jYeR6fK@t&Euwma!ba6j8bQj#XD`V3yQbz-Zg_TX4)>zPEO2%9YeeKy`8OB0>tVc zbh9UR(%1iz!`+8S?$ecEB2_SlzjiFgMhur(GA15ht=R>sB9^_1@C?Y^<|LieqG z!`p(x*X=LAmT7Y5zBYvW&xv6t45Ei(@^xJ7yVUb?IDHa5FoeUU&kaKh=GMIHO@d!H z(S|>D7^6tnS5W+3m(Quf{68&#tN3mx52m6o=DH6BEXs8u$3VMWma9#jJ?A4atlsNb zFuJ)JOI@8uoNo}iY#5iXK5nVR!&KQ>K``7a3HC?kR=uu;g)}Ryl*OhjEio?_wEgV6sV6_OB=_2Q6?= zOcz>q??nP<;Q|te@Ku-vWGg<#d2uh;ExbTBBBX;$B>!82DH!D>R<^4uaQD`;&)|hi z_`~Se{ltgByE=Dmwk%RIvVGb-TYQ9T|4#@ins=SrY3P?u%JJ}XW?uxQeU3ewPa-%C z3mA>=KU+ViC`JR!LKLkKO3~WnGzrvP9~crmZvoF%cf=BGoB->0TbCP76IrNMVhy$OS z-u}Zon2G7kjTJ*fpdXX5BU!OO@CE9Z`*bG?HicxFH~Y-2e7p!;ad6qpmgNRDPpL@? zkQP1oIIEo5{|Km^urJ%pAH#)x?`WCdQI+acGC10ii!Q4C=5Tjn3#MWg+_X+Xkp666 z$%svN=p!*dxL^Y8-I?V~2cCI)Xr+X*KUY9E&8gLB4(0w}vAgI^_{R36Ng(EJ5pm2^ z7HgGs)kSvJ*Nf%G&dyKlc|s_rEPZ#h=Fp3E#u}K<@=p#(Xa3xAYwu#Z;O-pHvyV}U zqc|MqOeDv?!f*c3WnwzKq$ymB;iPqc(_i=E?=v&^c9*y%r`s}^m$}7MdC*u4NKy~2 zuCHY{K%L9{0i+W8(M&d$_?UYZPqx!%Tjn_mh=ssCX^;g z`+T_?1z&he@}uP zLZAN)y)3^I3w0L>g`Dl-E%p{ZJmF6lncla$DPUBso9KogoPV-uBGxvr3~oFecDxBY z&p?vt2ZljD>_!d;!DJW5Fx+)?Fs!@9EMrONSdn@BDxTa>H> z+f&cGIHy9W%gc3Zn>Mc;{R)H!Mk-m^WFlCI(ssPcT3kz$1Uh6l3`hbebS&YhRu+@D zCxh>)5Yq~=G*?g-^}a~svGA8@Md)KDDL4qSBEg3q3r3#4EMrk-bWu{KMXIxH9Bf)5 zLNXy%I7+53%1ED=M%(Jq&vKDFrB9nIHk-0(usI_K>8la4$@Lx?_8 z0QY3b&gPx26r9doPpM{GyLh-=E~Q%8nMRC({Oj0{02F@D&?TdL$mAqvlfb<_i@ttg7aO6ccR^<)W}Ah+L=paj`gP%H zwd-KRpn*qBKYmmJ9+7Dd3l{!)csDEK@tvqE4#H3P`UH9$ERh^+zZ0_XDcNgH zJ{G=&TK?@HY_BFe|EjB}m`sd6;Thd2r~ zx2Q0%CZHedwX?8zcC!A+e)G-*(wvgE*)T3jm+J$qM72_AZzN`ubAGK2&|KJKx|kU3 zY7bpip_!}uAT^=j@+mzqnub$o8gmrvz|cQX7WcHVx(~lIv>LMfXQ)E zo?%^4dMp{V$B-h$${1oK7vZo1Ra|PDhdW{73E4lFNF9{l+8SSB#*^~kAs83u;Grj_ zu{^Wrs2HJY9}FK6ANEF&-tHU4+c;pgON_t=dat^LB5Z?k24dZoni0 z@{2=pSEroA%f~%YrZp?t87ITpS3{?Dj;_Ej2JP79(6Um}1NN!E9V~@br9Sg&#(GGU zljs=PYG9UhL9lkYGw!6w47c+h=MOTYaV&z}FHiJ2Mx*-37}R_@lcxdLcM5Xs6BzUj zT2ur;jxiK{$}CqIv?qkp(%5o#8qdVSxdFESg&& zb{pybLlKXwe$K><{jK5XW@*lQioAmaHUH=p+XsJ zYMXn1le5&mhp$qhGl#8hZvN!Nw!lGZuF7RyvN)%H&$Z`2A;YA$gMc{?mx7JoIbO6q z!;ys+YvB;`zjZkT<^!l?hb(*^gwT*A6;I_JGZGPP;)4*J7H;cH`KNq00TD(~JXN5Or9W`jQ*_OC~|l7+qgDdY*Oj{ufbg zc=DcirJ87*xN?H3V=QM0cxrIObSx@)S5(}uW;mk7CTI;nd^ir5O)LSw$|9JH_-+_t zKvoCNY^6rzooD;{tMDRuzi9@gr!dMa>p||Cn_mRktOwJ_%&GIaR#dA9UUfuOW>3Ct zt3@$lL^+L;w1S?u%3=Y245 zLto2pvUr?pGq)f3X-jFEZlU*$d-d>bs5s=%LBi!(|0Oc84=*=ci(g5>5+3?4UxE0x z)NNrv3~=B4vnUxW)>+8z21N`0T{t1gvhtp-xtq@Jx%ixc?>MXCX&IsJ9OrC-3#RF^ zV&~xS#rbCFo~Q@LAs8+?+Gji+-fr8t{Q+rOK@f$taocs}UcB`=45O7?42N=KPA+~) zSm?hs-}j-R-(}%d(F#~}8hUM~ayE&pWZtQ(>DW*|x5AOd$$$AIMU^1kRTyyg^+cSj zziD?fyeDS!vF#V013%g?*AK^n$NI_NZxi3nM9^~La>z%a;k|^@#sg{4ky>B$7_mBJ zRG?)Eqa4W@U(%$J1(XXR4PEzGoUvaV4M~9I6SQkSwudGa!p=?AT9chWtp%^?tL(pJ z?r141I^V`7-s<{}EZny>MEtr34r=ljGLZN4fYGeZ2~k=_pdbE?x{aRs*8TE5ELk~X z7;)2X?It;V@&RS>vK8_~42EBpfFPz!1?9#f9Ilmv-yNcfqXVS{9ja|&B360+wGu0^ z!YI4KrJw{9{nulrS>kE>LRv+yapqDy2G0bbn=`#}f={~M5yqRB8*q35HQeR%ZC^20 z=)UUjC(ZDYDP#BkUyacoB4J&+rX)fy^zzmeOK9bsV+nsxH~pD$4Iez0<6Td(vtN2?PR zz_RxDAwea!LH8R$Rd*d#@KD>$W2g$wN0nn;@+vJI(AgPR&!6f zm&nI#2v1EmiMZqtoIzs{FegSL_sG*kHHlpe1h-2Q3DKi@gPulbCilFK=fRwa*P0Ps zNKac4#oEK^{p@V5_?dtBbg5iCDHWA^=WJd^$1-yXXqJw5=&}L!i0Ibn{#_5*r4ajS z-i$}rDqxX@)Jj88b z$NKA1$4~xzaM_}%HCKIVxUW^@NeM)8%sSK<7%4zz8gcNg*VuA1(M8~&Gj95v*B-r@ zNvrkmQ0m1a0kmFndC`77iOCaL7nYqvcx~@q}=bwiIP*S<0x8?t&8g zUSc&`Rn+Dqk@2?=!KI0cTNzdz`*cs~v$87UGtwFO&V?AW&O$Sk_i& zs?G8>K3iLfLv`CEy4Mr$>Se|EDn&R1^g~mbS#6*n8yKfBLL#8X5IV1-xeE+CI5@lq z$P+{0wk)DZ z&ZOPVP#&7?7bt(YtQO0*$%oFmBzv*s=;;V{9P`m46r1Mu%2SX;@7ZNYtB}z^C*`~| zwI7JtVgHaRWA%JY2n~7HAO){Y;*oq97BFBS^9t z+mY3J-OGZhQ1T7#SnIqJYC2%h&@$PYW30cOOOVKL)iq97=v1kqx_1bHzI|io_s4Dx zy7H#!5*&6t<{er8tgXVr-#dK|(|rVv6Z4Z~f^Pu*8giMAKG)_7GSvx;e_|jh@nTKx zN_C<{2(tK)%Cd(;F2Opv{Ax)AfJ=bF9|Wy&K;Z+brGM!yAh{Q4x#3*G77m5!a$dM% zyo~+g^41z=Z=60x!fu6D4;f_K4!)c0`3Fs4B}rCJdN~=qr@D#2vGc%jDiMm?8QQlb z2qkrt>(6vM&YuVH8U+JOrOb&TRdXgC{+~``(m8oxW)kn<|NziEoMnU_l&{ zqj!YE!y%JWWbKth+Q;GB@Go5`EDe()5-!Glf14?4>!kgMP?{N|!!$0LS;goWz;x+KmA_&Eyt4VIXr#o_fQ+H6X@;2B&aTF$w_7$<6rH;=X5o7@(V@R= z;39|LQ#VnKtv)QZ5dQ45G&T}Tt=Crb3ojxwjgXBgMR&Z9?8}FVy~COR4x{*PQ2by^ z;kzH*LvU}yc44-sFL6EY&Mg1Me9z3xL?K8s75sAGCn%RzNpxLNe!K2M=lqj+Cdn+A z<`>$`WO(hPaV-h35dmzER;4a*R$IBbaiU9rsnAv+bxIu6-ZW@uL3pt@#Twh$@V1HS z+s`NqT1CSVa<*&U_9)mOPWAZ9;`>l=VgPj#)3B}@?gal^wP@)+yFiy!@n`;cIFcLB z?-&9;)^}XYS81ta`K2^}Yt)#_t)PpjC>{sCev&t&IVbCy!=4Q&sGcS-#FB_6*0rwM zY%M7;*yx`3pFWXHUVkE(w{GExB_Q3a!iJ;OJ}a?0(|#05l6X$LCt5`NsM{(?f5o%? zhJ6?Q@8dIrEB?;Z3Jc;u>Zmpmg5yenr4o98-XkzwD)qx%7OB?vRVI#?pXNPFx?=sk z+Th@fz(EA{b+1_8Ba9g`@Gy-K$7AU7FMd%Jvbxq>0T>1do`hw5cA7`dow(P^3w}G^WV3jKpm59 z+vv6`3Pc^Al<{`W%3Z4(q7+U@1DE3eh+d?We@y$J%;Glg0}wanG*+FeuRr3VI_n5u z-Iwxs0rNR6%$(SX;a|ocfmETUwHA`sPwM_zwFrOZ2nnlX=;tsH#uoZa z93Ip?=Kc}QafP3qxFD#|6ii?15K&ojROAs6g&WfsS;LG5j9DQxgWNO4gbWcH6LQ*z^Do9oq3e(3oRA%YCX#WRoG`sWBE6C6%vZiOb254e=?QuT77H4 zf(2->2iOx>-|yH37uo*P$7pevQNGB?qeV9iMNEc8A5Au=&+pb@Z$K;a33{D4JFh@mT0ljp`wlr zpS5#tSHB=UShQC8xqld;Q$xC<+!GeoFm}W_)tEP;_E?qWN5pP@`J2F+w&JSwLNE|j zS@;`ia>tS^!@@JYI$EaA0#*YDCF|IK_@#J1VoHbOVqwg zO(Ao)^UT6uhJz;uR0|`epO<{LeJRCHv*67|8)L^WM2^S)EYnO5=pI^GmRzzTd_m!H zl&C}1x@4GU>l&$^hf9!y0}V+SM(XYlwg8%=xN$^2;*I)mLWAFBe27$b4J}=j$Ouk0Wa^zWw^K`dG zo2Y9lzxv%Ccyb;OAof(QX1^H4aa(O((I_cPhZxI3pOzlf{Z2JfT7{Y*stQjzX+xE9{|qP1BOGV-0n)uS za_gB^n&W}AXKtb@acHbsH8KuGP~$foC_X4FAI4X)4=iOvH}SMMLXdr0wbAo}E7yI| z0MUkxe&BLM_I!p-a8KK@2yYMz_xlNAx!7M_caio$sk(o;^7`A*%#Y%52VNmFrP6F2 zEjI%ll|Lymc~!e>zEz`Aqk%ly`*K7TR!6~n{(N=NAhEEZrGrjQPkRBGMGW4*kV+959dn5m10!K^Da9@0>MrpMx)O?!c7o<6>L;hYaQIawz;&l(BB zf0-G>D&^B4d>21ny9D1Ds-z;y$q7eL#4X9<|4G;Fy_1-MKXv|2(wRKjlC z+0;_AUiA~axAF1L=j^L{KT1A1wU-v~2qh_)rA{v#5C;^`Y4{1Wh@+Y^O_+%3&gmw~ z;>K|rN(OWHzcQ8aO&2Tt{IT+ikx^?AZW;FXsoU!jPGg5Dit$I7(bs0`2~`%inu1Rs zs;+Jw%seKuTRtwVcDTJt#BM?GEsQtO3m%x|&rha|SR*JEhj|@cEFxxA{j9i|R%P2( zC$%SH`$+kH;1h+}U?;_aiB-FYy#`P4Uq^DjjLAYO>!2gy)-gV{V#$F&Lxy(t_F$kl zHdJ;GC)!M#GXZ2qlRzV+qXJ@%HNapB933WM_dEY=qF^lY?+XgpNoDZ3fs_cEl7GJm z+m_-BTzz-G@u%Znsy9X*S!P(TGJJb#IDB5U?k}d}?+1u@-z}KtJ&1kR1?^TU0;T4Y z`4vQEte<~y$`iv@bzs>9d#A+I+@o-q?9vAiZIj=qL|3OTl3M_tel)f5x^eVN4BgH5 z1o|ZKjpVCIiXxr9uF34e`SzM=vwo$za#Ec~0*0fJj`v+yuwD+1UQ)`jm$zPc{**8* zveDeTJ3C#j412tg}kaZ5(Ts~ zIf5{yW&A&{68qTjBlMU`V>2d5!RR1;Zebg{CWJobQ z_2vg_&9dj8BjL?S;nADlquaOZ3V2B1LzP37n2?-|%yPBUm+I{^kS5gi+!+1K87ETc zKKq7wwBA?j)cOGr1$;*tx1E;w4o~gXJPLek>Mb#24jJ=#_&pY$iflY`>#?`~Bm#o0 zg@u=R0y&u+tu4X|mKCC#uwt$}CqH2AwtY$$>f>uSRU^41fKR}DYJoyMKv8qikB9}C zw0M_lwRibd$B*T*hCR*hx|l1mvwM{Gxk3b96?uKyHjJ?Hc1%KG2F_GPSlSn-25mr3 zdx+9Q35{IZk!0~s5BuIyt z9#x+2u>#B#SXLKO0eqJA1he6Tjc*K`PVu1Dz7*{F+&m;p2cP?_s$@u z9z`-;j6?$IS_&z?iIB_ruc`d|6aU`COb~hlPV#as77!v`xp)YM+aNdP>p@lZKdw^h zw#U{G`VFv3fwU_^ghEjB8CB{3x`c?2vA>i0&+`eT+pS_YD1oMOi%3pHhDGnOo@&Gw z2H7MxU*XE@-gYh!_G=k@(5%bJ#O!u52?hf%OLQ@Db%{n!F_5YL%yRW-zaEMCd8W{Ni3_m_VOq zJt62(61FO39Ou4)Qq@Dn^ZFE_qOBF>No@F!bOYN!mpoGC=u1jjp9T2OW`_rA=Jq}%zAM&pkj!DPS*>EQBUpycdaLpb^`dhA3d`z=bH9TbOB z;Z71|OH2ctW|jh~I1N+&-cNJ*%-II(3*}yDZFs*YC0L!#ApKRHyGe-Qsro+3^5?#J z)p|QA!vxu%Ut6v&9+5ARMDXM8c6du|dG3CDzdc<gFZE(*2e~pS4~8V&cGJf>~t- z4c$(>~zo~{( z5>n_Vbo5|b4KXiu@k^zvzRCMeSTQ7hrBC|uOHiPc7cpCvScN^Tr_SDpc^nOVkEtDn zCQym3eY*tiV3dZ~pqn0EB2xioHLjN_cV$;3#w@Elv1~tn?Xj$q)DiqgqHk_Q{~l>k zzc5q!Jwpfz-9RK+Y8Vu@H5=SJ6^Bg${Hw}P^9~a`VBh=gF^CPy880C0{D)*b z>A5un2#s8RlsS2s6okLBa&VgK=fo%)Cx(6p8VtmbLg{bLU~0xX*Ni73Cg#Td*I6X* zU{A$fS2M`~XU_CQBwF_$XyYaA6*C%o2CjN*wEdqJz&ljm73Q-RPV%CZg3UrD9Vh4D zy5#kra@~DTOG!;F4xO+~1DWuq1Wj4sg}1!@xsZ?fZY`5O&40}%ugfAyHoEhH)Hih|7$WhmcM;}S+YGOv^*Y+ zg1!oplwRK*|Mp#3XNrySk%}A`kP&oWn*<7XFr*Z}|C1qp@%<&GWJoSJ5=qf(_1RgA ztLHHU(5IU?{^f67FBQ75bG{3!*@Q(F&?e|Q&UgznB^2fZwoJq zD3Rx{OTqTBi{~lVoDb``n3yn|TE5XV1!?~E&)(0mcvSm^M(3EU15Ib_#5I+y(A%R@ zae-I~@we8M8lj|G;YMmma8``$^8dNQhTjss`cx)3f8?W5Y`4ZNFEhWrhyt`SW?Pde zZ;#VE;UA20yo7f9z8NUre=lIJ&_sRe9ZcH9lXMnqFe7Rp zCAR+{CiOOUq&%>8>e|jgIvze%@|IzC_VZ5@_0ZZ9CgXATV+fFMMTT>dVZX4AM9Vy> z0%;cYXum{+@0- z2ZrVMpDb42njA1?X1V3(=CJ1=ov@yvCSAvk<(G^J$key?J2 zG-5@>!Q#CR-ifT}cfcm5o?4N*-5SQOa$pfpn&RJPX7FNVEhBI1N2{{VR2WmJedORf zD^UDYALbtp7#+9FN55*xH{qcrDICpzW~CCAHd8q&rK@;sjh`5wtzpnuS7`B4`*r@o zto2+{nP(D;RhYaP>R--I5U(9$;9<7g?ZWlpff$EozO)Juk$HbI5%VVvaHgnTOKfU*G zj7~bNA_ws9acr?$;_;_uUS#Yp+rBh+{%TYb{cH9`ApS>(g9Hd8foTo?pQ>V~Trgr= z7*M%vws^8C^$o~bXG~G6y}c=e|KRwUT)q@P7E9J>ii{a3D3M}DD?ex;RWfjsYOcl* z@VX88z!iGZ2OwO)p{_~miQR4&_;a=Un1jF!`aAe|;#UK{4eMyn|l zT$>5)gLbSU*;Sbw61fQX@@;2!1hPaSWskZ;i2GV0!xg^yXPmMU$-S@4GR9~otjh#Q z1u5DQ;)N3E=}#lKVzqzfTdC%jIqcdZ)~0<;K^?jMAI=74c+%DUC|E45y5S z(B^C;#eM1egZSz5*(fO~H60p)$>Pq1bKTx2q1A8S|HhRESybw(bUqG*Y^Iwv1a1W$ z1*cD~d8$m)hqG2z!LWCKT)W;Sop+gBtYBpa#9=d(@mB^Ay=;tS zuC^;H*kHF%{d)|)KV^O9KIn3IQD3yA)zq|v1JX=Lt#VYVl8bvu&eJLLW0QDPH`9Mj zU!C}db-x7q3*YYcSL}|AHQI1p6J_tCuKf0~kO_IXA4q&Spts!N&rtEs7|a%Vvh@l% zhA{#DZ^-af$(t5LjdWP(vHe0f;7VF8d2&Gu~ zHz#MKfAl`sDw@Gme>|Nu!m%#2Qh8zD^}Wg&u3yqm`U*YrzyEy-<85gJEX{lzUWPLfzV3?d0I32MaCwM-=PmIsqZqlh#E}Z3{P3x zR`i62$|12$y9U7sw#}@EzP}z9*{8w_&pyA;r}m1XC57eNbO)`UKg0}L6y!)}?Pcu> z28oZH)62;)DItwFtEJWkazc&%o;IKOI$@y)(aX0HC^B<&-=N|Hss`g+~Aj}`HU ze1;512}+-f^2wHwF@LWMP=q=<#`5? zruaaKH_k!_X9-&AZj|9mijt$6x{&Kuj%=;Y$#aaHYC<%F{YX{dNH- z$WHnhlVg;8ez9cQpQx8xWF2nZEf*IS35SkDr^YIb7rI;i=3Y0xz~vTgefQqvmigy9 znxnFhY_F&8RQuZMklp=K&-;s?c~No2H9JgpA9_z80xt8{Cr?(pDi@r7IJMe{8*ZQM zICAYyaoIl9TpO-bB==mA=d4K{Uw;;iu)>FPhJ|_AL$-D=6HGW+E#@0VSglNIe#~L$ z_h}N@KQ1Q{S5fq}TibrglH6CIpsj=?ZrPg+eESw9PKB}fNEu$N%=BYiqIC_6$A^|| z|DpVpxG-k)s#Prweme;IqSl)-H7OSGCacA%!^HXI) z<%(YFX7Je4TV>&EYzScdnfAqyfl5!8q5tZC``&*Bf;Y%oG@Sfoo zgCA&DI^9;naP)ZZl!siZ_4Tj#F|GO1lGyTVLdV)rMGJ-HO1of0q8SXl9{a_M$j#!p zOLwb$k>QV2(|yirCROacR)wvHy_1AqM*UuA{z}(b=wK`J*&$FpEg1@9-P(i&SDwbfeA&=SNDw$_b}aK) zk5;Eb8t~!wFA{i>yr-|hDUiSD9}wCGZvOdan(t zy?$+y$$rqYqBMUPsihflv^slM>=GJr@QXugJAVZM8a2#doRB$Bih;cJb{oCg(X(C*Yebk*Mtf>z4+8;C zO#Iex`k3wp&TA88yz^ns_jd4>f0tgd{VH$qRo^d4^q<{jMh@h|l=r^(3K#TzKXS^d z`t$NSEyJRmLnRuM=1uM2lEXOsN1PxTXlbFRm7N`E%8v*xg1st?>I^=*cIWq~Y5 zzXLuL7!+#ol?3xfEun9s2CTxp31CV^ztGYJ@%Ru7Sw=&WA1AL6W%}5QMzebP99a&J zW`j%+aOGkm4iRv7V6P0JO*OC$eyj!0t!-P`Pkj_%iVsQFb4CG_XTkDAFxQINJX?Xb zCfB!Km4U$zm_mZIjvjMzx73KsA>sv82PxAi_Sa%hQ7~oQwRxW@iCyyAquJgX6pDa^4m^s!2#j=iQC1I_py1-5l{wha})%X8=vYTb zVjrY&Z}zKnXg1k%nZo`ehl4~zijjW4qA%U|)_BH~D~393$Gxk7qa;h$kFe}X8{4(w zN^d^PWT_mnV-nCT@bV+HwM1l{GoPp`)XhL##Pmt@znNO?wBd~V1vlq54^15q6j$2bI=oXQ?2`fO;6s3T9$7EY)!tLR$W>gIJg=4Gc*^V*e6BAs!-X-q{ z6j3Zk&~b!^6~%jjb^HNt$fLsFzPe}~e^J*#7!*#j59iMgiadF==SOwuN{ zHi6~?#V{kx(qUFQy{4vHyK94`vk$uW0k3bDZn31Gb(z_S1EJ$+wW^D_Aok_9#?Ci{ zelD#jyV~$DHy!%Ki^TeFpSSG@&AX*Pa1Dt>S5(KtC`38bP!GMemFmg#eXm|Kd_c71 zB-y&SMfl#B^b2KB!zam%jfbc2s2Wzx*3L=DCRr%0s;-26qm#!$K{uPn(xs#P9qWXgl3vU0mRc zl-6r>!k+vJe-|FfxXFuek(C9QAf*0^yv2~^&rCFix<}rohlsMkiLiKOVrTXSSmAfK zDa-h(y>)|eWp2f0WUg5E{`5+vXpgox(CO*3fKYW%jy}b~anzx4-<%}tSmf_QjS!Is z3M@C|c2;C65r7nLt`iPf(@M?ntmVUl^qd_1t z^X+edq`%S#8Sc4YWOu8cv94TEEp_4MzZmvk%r)JJ?ZKa`XT)`X_ds5 z5q`B#u&Isq?;g9jikS6LQVZUU{JU-#eUFVd`aXAT;1F-PoRlxoV1&q)w}kN9ErJYx z0Efk>Blrn`rg=LaD)b_8-kgOB57*A=5A6y+d$yhhvpAluiN3rCU=mViN2iUY-5hYa zYmeaFG;?Qw3Z@RV$A}p23J*2Johvy&?q*fTY<(h!E+#Kco#X+uA2 zBddh$J?Cxmf|vU;u#dR)PLNb5?i+3N92yTH+VpYxZ4hf5z8yX9&yVdo<$YBo-LN{j zG~bhcwpynK|7KniG$4BuKveA#yaB@rdj_|o4rvQDlS}&BY~yYWDJXpXkbN!ZCbzZ@ zckgk;(Kce^w`I}bq++_(IuiAO@bkp*i=@-424gr;#ln)I)g-F^ilUbkJZxfnaY9{Pc>Uqsm^+8YO zV6*jZGFZ4BoB&oKx_1%%!Edq>tQDM!Mwlj#jg6m*6ik+mhz%FHMX?=^29O%AmpLeh z1fNU=lhA0m85J8|{T$8}l4hNX8)7E*Q{|V;aZRHr_JfdNa50BK5{gm8bmdn6z>BE= zfD8S^(;6&&+AL@^#)2sSSzYOW?kA5m-TI3yIQL2bZJjNn=ouj7`&zJUJ!v})W*!=) zGc=FCm+eg!xq-m|^pDFG#u;<+qz^oOp_GZSAz!Ux0yueI$2Cay-E`4Qqjf&TBfSw3 z0L%4_JwJ?HM0{)xV)0o=Tk?f2zli$-1G1pu3!EZ-Zp*_LTqwRfTzhq+oFt>tyB#&I z36s%z-4J983!G>~rQ95RnhOcG>hco zkjV3B`;RhRJE5%*vik8L18-_^>ag4 z50(B7y@5?c8AqixbHCz*)A@i1DMp)#z}Gfzw6mVe8@>tp_o||Bd`{W-bDDAGUsW}C zHraQ|b8Y}{1pK=lUy%4b&J3$EZ^2DX8ld*InBGQ*)qDg98USmU+8m$~ZY&=Ht|vaP zOZx1Gt^c_JUz)S|oVSoc;au}3+36Zrg(t@9({=rAFHdVn{x_jP5aI?qyev-#NIpe% zkXn$Sj88^x)&IRZc-z7Y*>@p~5udik85uUL*Wpc+iz82_cc}DO@kSz6B6{`jQmAHw zC-Kj~#@O!rXjnU@;=r-Kr!P5EDdYFqZLFE(ZJ4k3#;o!wSESxqy`dE)Vx)cTkPMPm ztbZC)cC*;gf8mqlR3t`WV7~4-nA65b(Z&tt{;HZ#00)|$+asu`MiP0E!u;7Lz-A12 z0}rmKj{kTM-Szrp9gY=D3D!bYuxO|~XHq}#RA_HaG|>x6gz@4TCYsfi@xzHc!*p4m zYt)`1oWph`2+BOKvCizv`mCO&VT>zI8o|!FG2o-h#!_OW7bX5kh+{>MHy7IXO=G*e zth_u24okZ1F#zD|1YF76%Oc?sfdBHAmUT9OwqzT8*xk?CM@gstTnQ5?0-=HVjutc> z1XBP(rb3A*ApUiRJ|CC;-y}EfGvsd6#mO96BB4DuoJZV|IZ-KhMoB?3a z0PKU{<^=o*XB}96-~XI6MZVmkzzbrq7AsA@-W?8HB%=WI@N!rbs+E+b=ae9@UObu0 ziS4BJy}oMYY5lV=^GowFoAWJ1F4y+8yFI46Q6M_}^6l^i!O;EC8kZ9s5!YXox$uNj zrzRoU2SS#6d1J^%@9IOQ;#*4lwM|S&O$Le)tQeyK$cWX$fAQ-_6%pyHSAAT+jdgi* zF{}}=C2iJtG?!E(!pq_e+iA7{b=qEdYe;eCYn$43L}*WZs^kTk8i-j%Gl#1%uMR(K z0;8sldm;HEOpcnePOCwb?1gj{c&a1o6fzPShRLMfD+4mS`8m{x_8)xuSTieA@+6Q= zqBQ-#fj>Wb#C^mTVm&UHuk&WX>obUPK6$&L>Q}T>Xr>*jntYBh9GcEca31=J4km+v zJ>gh^`oL5F5*Te?9?|3^?l<8lUW2>IURVTkSfstkIaU40Qm zA)n=*^Bvr<-KGv?&W?o^JyeXqgh>g)MonIHSN8Xzt_BJ5ouOW~rsc3=UvAmYPA%nf zH3QNXcwfayu89GZqZ&Owjy@-FM@f*Q{d~O8vp-eD_b=1m@YIe1ez^l?PZR?JpKah0 z6vkzP;CA>**`(si>C^g|o8e->cGFlZB4qnge$$do;o{FdVQrGg% z$Qx80iP*w(PJvp@>*yPnGuFjkf{u`1vwI%6`;l)n-k4ykE-~>0*j;ps^jSSC#kTRK z;`{5}5c2GFBs=gy&hZ0=Ip!f|mI&xLxsz!LnDXbHV;5alwuN~VQ^^E%)-B7ifkEnXY40>K_uFIJH6ye!X&GiA^}_P#fbvmy?ANA_QTgx zIdz|x{8^~RVc^BuqsUwlGVI_LGw&K4wSFe9==YfHSLM``LSWMh)~`lUnn*2+(Zv^` z@uNFVB()Oa*`~t^`D8rPJJ;;O;jqbZ;@Sl6BBsms6P3KL;!ptqSm64viNMp%=1cn_ zfFuzR5aeB@jl2bPNnq~3L~WZ8X=Jt{PU#EN8M$thV_;f!C`J5E zFCdFIZk{n>DY!Bvyz*)s^vsa@{MlG_UFl9iyK-1O-uC|iXUL6>i_~sQgwL!l7mIpv z%FdXr#z@k{lXrrlNyEPPMuB?uZN{{eXU;jl&KN4mBh#XTJxBasB?{Stt;uN-fvRI0%YZxsq8ZlZH3JzMY6JwUU|)A4N)%yb$wum!qRO zrhX!>0!JulnFP-cT)=sKZdhYBCGOe1QNp@_r3;=q`i-@CboZV88RsP7?S@6wXz z>~>j<^y_bNhUxXGKC2U1qC&R)GwPE2=LDq77ug@)K4J|KLrQ3{>)NNWh!|jopt5+M z@2B|l)@T{$)+Kny(3Ji;8)X?aFYah-p54OV`tJWRBzfAa2|lHMv%#2Bok*#m)1v<^ zV?i;U+v#fNBL_Ek$R;n-fB%78d9eoLFsWx!tewLqQ@+jUQ3XMS;~5EIzsL$|M$pan zRNw8J$@+cDPqS$HA`Vs%$nlrM!;ikN4e_{yd-QItx$Ra*wknhJkn`f5l!9dE`!4RB z?(}xT;ULGc_8E$-yXp6wyXjmLVv*#0&R=_aBvFIK(toSQNN2#X{2HmVOV?oR0le|~ zR^RvaD_-wj-hQx8z7V38T*jP$Asbk@Mjsvoz%mAOKH!J}c6OZizjG}9nPxszZ~W)u@frL?3MXzS2FWhK>{;S;;M3PNC>aXlQlZ~==~FM<|WKIBSgG@oM1bB zRd&|OcYm7->;r5tRE+p^B6S$6x;iGIwz7k~p=8x(of+L^qkF;DWFH^&x=%5_nBnxo zbRNDpHQHM0J^iT!P1URD_~@#wqm!p!0Va4a=wndk0r+YG*0m&wqvqAx;PtaG@CqSA z+Ewv_Cwso~)_}`#<@Jaz6@BOYc|2e8FzdJWN2UZ!+w|*KWieX6_8nH#(E|#Dm;)+y zR3Y(eR=f*L&O?F}+l8n8>EMrsuMOu)B`4;&Ibv=w1kgx}B+2`&NYqb%*%zT93q5So zN&?`g?QpuRwafjf7c<6|GoQd1b)sA?v3Y!xGA?yFaKM2+AD0)!WYoPi9*<7x#qVjHXEcMPyhrI`yq1m~7NuSep2Pr#KC^J(3eNWly^MN$Z~YF4!9az&*< zphOhLf2`$uyCXz-5K-K9K^Psnc7L2Fc>5$Lxyw-UF`xjqMopD@RiT3ZqS5p(hKVLUGSSU63D?SM@ z=zd~*ge0I+%`_2aJnPBDR&kBY5pj5zsVGN>hasv#t)o8Q6?~dmgZ* zuenutlYy!^@B@|EkoZk8?1zBOp}cX21Z)uB4x3*1dtfoY03%gacUK8!lz zRTfPBjFS1vFnaj-9sg~3kR%>Nn~T__!?LEkgo*|ieIze^nu&189y;rSKB3R~KED>Y zn1&9RSDRMPTh?#{?&G@-P7NI@soJSA7?KjksedbiQwV({TbwL0Rk6ddWz=jn^%j$i zzZ=9~S=t`hU#^I0OlybSa`Jn17}~|v5Zg3fawBhV8doXt&#xLSjP5? ztTsVQJ!YMmrP=fNTQv?0EvgL+kKtyYjGy0jo8p~UofXUt0zAG!ZkGzw#OAXL;A-@< z^jYEk86Tc9NvT}?c=G*QV?K0Kiflpp9cYIG2Oweeb@R3Wd6>_b1}+^h+6iC*%meBk z*unRp!taMBC)KtzytC$ZObJmDfH5b5Dh=UI5UYo_SM4w^X$EiJ%<^_5Ajs$zQ##+ z*Jp~5fL5-)yHk=n5YZOx$}y~?n0VNr@ye1G%C-RHIz6|U54MYS(Xp|y#Y5)6Xm8=i*AD|l!o3emOyH#~`*3O<99We#|7wiMKSF&i8P%Mhauh-n zj>D2DPB|?k0&vWLd;%**9U0-fKrnNs!Yo?^8rD#QFo{+&s8+t zyrb@mW_MqFfY)qVcW*AzO4Y^|A0U$(`>x(uaMeSKPy`DyP6 zKa0;2Qad~-vv0kVe5_J>Sh5V$L7?lH)=Y_s{LSYFK2UghWAJK@FeHoTZMkuCP8Mn- z!B(XL3Q~Q6^OYq0d3b{9{ii=VHsHa43q>`Z{zh8x)D~KLKBjjIW?@bVS4-}lBF^q1Y4+7Kc4~8u}@31;zb92K70;cy>-X>4R&qN@<4T9uOuSi z#TDH)4=TBt?7+LWrZRxp({9Az<$j8k8HiCoH>O>b78X6Yd-g=H^ z2@U~cG;pNFa;TJPf)T4Kxt5LMU|nUTS#cL zkQg_uGeHw?-Z`yYi6OF~u%+lxdP1wwCAD41=}Tja$!Bh&H>{*+fCv=@G9$oW2s%w( zJoSJ(Q8jO?F-ezinl>7=q`Y&UQdljA&}EK=iOK%n4RIIAVEg)y{+Rpd{4l52+CUpD zeuwhHWspm@s;G`30OFrrstg=$7dMsFt3L7RsQxB>#uX9_5%cQSgzik#IamLI5Is*0I-yi9s`ahqg} zoxFY7b2hZad#u{HTk8df+#S~Ew`z9j@0t^Go?tiZ`Bt9tR549({g0(3@txA@{B-{J zGhX&74CBAD4Wo({(%c9&r<;w;aD}+K+_r|Bd-Unt_NK13OwJG*9gtNpuO=pb@@gRu zH&rBHj%mn}Bz1KP$o9HnX_%;La9MqwqU{d1s!g(?_vt@x#Onw|k^OcpzAVJD{*)HT z?A=To;Mf%-Qck7F=it{wxH#P zER?v<_x^hP7P-WHkka)C!rP$YaKWYmV6#)C@&ixr?UwJi{?1OKu*S+20<4jdscC^- zYt*lXni>f=tMHyeS^{e>a{@w7W3RM*i!3Ah?4nCKA3~022dPza6N>m(jv1h)rvw2z z1+!DG&*lf(u-j&g_#`)B*v;(jD<;}K2$Ny%1m!1YmoY);Suampsx71B3K7!~)R(j` zJfjs{A0+RS%p|R7r&PJswVjX_@Dp~N`9SXj;9jp`Js(Tjz5bg;!TCUxF zS~^M;u}w@&OlDop``P-N#WCNQ9-Zi`{QRMAKk^V!jx(X?~7u{O#JMbt(T~K^-m}o9pX-k3K&KQQ!mUYKss0}sR!^c z{m;GI2KES_Gq9c?D!woK3%AgFsKLknd~N=DGO1I{C0K^Yi$D%x>`GtO1*(B6g7<^f z`zzuh0;gclmqd)E^NHuqPhSpiRz9}&r-q;RuSGbMS>oNLig6EBIew~-{x6INMow+N zFEgNlN=QgJQ%Rcf;H4w+rTu%v9K4d2y27J!dL?HlU)5sq%o1*IjSHd#>X{4w_zQ=Uh_yR_Umn+Q+1qZZNpnBrNr zS(bw#!VITU__(hrEm{@?6S9%u6L%s8A6TpN-%sR{52d190kd{w4Dorlyk!phZL)a_ z1=|pDadGg4{qkC;rENf;r=AC-pId^xx*nN4HI)k?lsA0KcmJY|IsV%lVj3Qi_-TqvrCH*D zlozU#agglHXZdX{(5wPsnFto=dB&Ox2-&xl6!&c4vxyL8Z_6W&H1OT4m{V!Rfab3P z^pH6Kf?$0q`W_A z`QK(J9rgCZaU8sRVIYl0-d{+-WPV|zWtKku%^{itU3^j;7&)-!4z#S5}8~R9tS^jRG{4=PNvs!?VK|}2ZL#Wyu5t)gldKQ zb7_ZRqr%(x4iqw07FT3=;OH}jPRLgnk52BONTsxud#W!$s~@wL!ZhKN~=9}*=0{g+PYhji@evF`W5W3-z= zMW=q$pMKQ!6Cnlt)(dJeVfHWCpo-#x5P8oK|D@!UUQs%=VT`=dfg{iXK<}=JvvE+{ z{g>xnB)@(0tg69x`Yy?@axvSfSv47EIJUZ9p~<>csIF$4?J*z)Fb678bd0!{=km_= zt0~xl{WI$7^iQzw&jCAP3hyUQ4&fW_qHMo}o_VW-T1IIDKZXPKtfB2B$@_B|;ZfLb z;sBbKfbk+g3UK8g`LlAx41~SlP2Sk=KM50_lSz7WI;clTLrj;c54I zgs1f%D|nZ!?Wt9xR4xb?u-c)%KrRG=tw&ZL;9J~;XWxWd${4CXw~?rlkQK7>%bA!X z0`*a5fpagGEiVyE*~@RxhfJzc$S{4nZ2+;~Pcc7J;#OWVfapQvr`fNOJp=Umd5!*N z0@Kvlz;g#~I3x+uz`tAGF*UxP_Y&N*mIkhppgaEr?W^5e9^3>t*FPCSjkLTC1;KfnoWEDT_ z6ZiV*1a+S~tr$@N&|MLom>-{J+(fmU4F@U%j1E>}>l}q2Z<1uGTrX-jYsMTn zg9e;*q*~_DcTy`xpg^%rooL&-6Q4+DF%|)}l7&Gss|{IsmcpDTSx))_WP9&u3Ue@! zD6 z{tA~-@R#~)YMvyfBsJ6ZW_4r`E2Jf8MFu9NdiFb}XKlMoU>?leoPZy5*ZIH9=KJ`$ zDwAi2Gxz7!Qx0!?{sX_+*2e>Swfz)sFt=*EA$$({=KrwSP(B1AgC!c)spE{Y!j820 zTFmOCWIeQw5ItCv?NFRL?LKnJHYt8e-1^{Ng;b43(d>6@S{*^NFfy(b-=0S3gd@>Fnv}X z_@u^rVI_7IqMT(O-?u6^sTcxwj0QiMMr(KZ>CIJ&>1DiD;LUx5xfCd&CW|H@V&O}` zUjQQ*i(X~6i*bias9;-ve0Ayx;p%ok5+mx2SJq|r2q99YBj()r&J{jfPQI|O9ut#U z!3Qx-vpTEuWRCR8(ziKEb|zb{JTUWfj&`C-7>imKU3J+dhofx!^o|u) zk{K=J?B4^$lNAv2%@%KVQV`g>5h^mvS3M-aHj#6=-EYcnXM*y#ZUkhAbXJ56b)?NP$I3eRmhAnRTH9-GC=`W7#RD|ZX)}-Kenx3|nYmmKq$+$^H z@Ghqr!t*n9A0N8>;D@<48RjgT zwhbpbvFmebpIJ#yhTCnOOycyqoS*Vi?FzGk&jNZjvm=f=8=10VZxTNq3-3Jr@P zdnO#)7LE(<57nAoRR?aTK#Ugzz=e5+qe29ICT?a{5@hyabs+FnK1*~>oEBs%HfbAJ zj1Go@R*`={e1!kklJn+|@ZIS{eEpKN$ zZL<*cKIy+tPljzU*HS$lj%vvpZ#U-PC9Vs4G@D_-J}^oAoq%)o?ys(<9pT(lAU_Az z6PLVvWb^Ha!%Jw;=fov@b3&S;P`^T2qfoorwhqIu@BKgtaV8I_<8IU; zL68jBUWjfE_YhijCExJ$uRc`*e=8Fi|$alnAV=(>4cI3yikxFvC1^C%pRqjl*;O+5c|*lW3PS8}<+T=yZw6%K)TA z<8QqKh4AB)@CYzK-VAt!5^1RDZ=X6qGwi+|T7Vm>d&jSVyPS?l zu7ml7fd=2A!<0;6hf?!o4t?K)ZMJnCk}}b zCnKD5^t^a~89kCXJ99Q>Hd`ym7HswN@0%)U)F7*Zht*qpPgw50{R@tpUXfyq4GpdC zvqkF&oU#;zA!%rTh9VxjBINK|g}2qQxF9^3nyJF~pL|wd>Y{=0H@VOG@srytiszfY z#@n6r+l9H-UZ4#~=T88M=R4@#8{t_UZh@{09oMgdLNo;s2`bZ+EmQ+Cwgf7t)k-Ob zI5jn?$5i@_T;p&fKud>)I3{#Xki5^(YybC{1FwKU>!_Jx%og1o6!z+S2ydM|7b5PL z(_24Xw*X#(BwnOULw?wu{-7Ni+kgR@uyd?u@X5gw^&p7+1P%h99?M8-%8PDd%gC+h zJ^i;hn9NgHL_>>2uc#5$0GE8O5cR0#&ieDoJRZj3Qg6!lUni$Yuwi4#hYNMbIm8J7 z4}kkAYF?O{F%f;NXxEdzkm|p7DER}2-07b{I&-vL-%t9e4MJOLj+^|E9q12+niu~v z-mY>io0JQr>~gNcakC(dyTYBGICovq{zQ3O2&Yr99t1t8ewB=Y!9&y|KU%g~H@c6= zH8vS;QOOhuY(&h-`SZ5Uz&Z=!{G(XUXIL+RgR?Ib4`A{&_(L)MrKrjK`p5_fK41kN z9og2jBw&Be7vIF}D8Xt51`=e6DMuzj;>v zye0W>4l6~XlaY$YhZ{;ULWp^hF*vO!o}L`kexX$wwiq)H$Co4o=eRB-i%r;C-*wSg znM15C(3@I~2e3lLLfT*bLERY8RQmgo-B&y*;5tpuiV9&^Vdoz z&d_G}R*G_35ZEvFQ`SY+YAZX1FsDD|X6Z9}YzYmuK_$17PFq4_g6(yd>TY67g?BojEpweKu{ zKujs}(8At5*Q%*nWmEXkt-6(Pl0@N0>bWeti3Sfgx=bJkOhVpa-aaIf&LsXc9kv^J z^vnVyM^%)Zs3iu>09HtyojTa}_;^MpLZ83u`d?$OrZAL;-|06!hg-Ki+qQayA*&cx zta~1pWUEQ~-!*8-v{}j5W(3+GrE@9^%jZwzXfR4<-q|k3SjU&nFj|adnSm59@VT-I z35^cK(}4nvh=ifckn3ZbfQX6u+kU8kcS7&W&rU+)zPYVl_cZk0{f=ej1u`ojihxgO#UjfBWl3}6jUN@op z-z=AjT2_-_S=)4Zl}V9ZJk$RPKg496av<5xw}GW|B>EmRS_flMWyf9Z295ty>D%;I z+1WMG1DIiOlt*9e77M2KEqyQ>34K>Bz}6y`+dlP8Kw>Ag&6Gaf=eQ*fJKq82%{1Zj zCz8u?>fc4Aki8NYmmV?VG4H|-s$$r`8;VsP8+-sL0P1tY@CQ2KQ~r|!CK#a6OXW{+ ze-7AU&waJ*dKhcp>d!^u-!W=aZnO)u4|Bh)TkBV48iuSR0`+f_rb0yH)2=O-0#rVo zzCM(!Fk^QfyBlhSTe{YW3{9B%6*+_>3{9{%{J+%&t%6x0ZU zS-de{z-q8=cZCe^hycSgb27kh(SdD9U%2776i%MbwZQ%+)r9yitAfr|KfIO>!mx>d zQ}M-KsA7^vG54}QyxgmsQvS#H;IV$I(6wTt$mK-r(RWKXtG ziEf{XttQn^ZFtO3SNc1SA9bXHWio=ePYg4kAF8tJPR)KJ`W^kLOME82@HHan>^JyH zXFHU)&YMv-qt)g_?a;J|i5MYCz?jJ0H`8scU8;oBmZQEaPdPat(`xW`Ua1K#4VoK; z113Bk=i$<8=fXxN`UbPRVFQ$R3zXkx?`}UMDC;08eW=Lj%QAv+cJy2`;zta;Izjz>SEs{W;mc-AlPK?<-jMFs_`6-4U;hM6~g;Qm+|%F+sYsN zMQ0YH*JELw`2Z|~g|%RC@3*E0QA#0URVQ`USCEz!N_Hf>WkC;|O>6YZEYyft1b&R? ztGP{-$Lw~!o{EVZ!;i~Renn>I_mcX5%3ou&)0RMtU<=};PQ)*?B73Mtq%vQ!FI#ET z_`yRZ?Q^0OII=JvX+o0TD2CeWe4h^CazjZWtb;jT>%)2-zabxy^#4ez+_&X=CEQKR zf|*o)Qk(3wKW-UuHeohP*sioe+43{Lk?V!Iz3-TJu|>7Qf6N==rVr^TAyfEMp0cNk zjDqIlf|iFR6d%W>`QdUWqV>Eb=3ieVk%g|?rd;5K9&wd;x%leXdFKzp%vTJE$?$jZ zN~>BK-SFu*=(`SAo(7w#x|5DhTcZQ=U*@SF516AQ$`vZs%#HjW62$p-w8{9a)GK85 zeqdjoQIQebbW%q;rr|flS=qX5VA~nk<&;PT6)xWU_s#|m&EZp`4kBmK+OtSTeWb6k zPdvVc^`b^FB>(P(aQ#e@F^u1y6!b04hoDdp{9je&B>$5g2{Yk}&snURV+7w0r@&A+ z|9MJLVRTeYTc@+yj--`BvHWy1h0k@We=@H@F*cp(&|^MBWh<7AoBmlT`tt&5fYwQC zS#X7w?bSckTdJKmzaHn{F{ME8Pad^%%1vV$_R{12*-h2T$BwG+>v$uy}l1|ZFxgLs&Ek#|(r7kbeo79u( z;QOxP&o%)g2gmf`Rq-Tk%#41vEMm)Ij;QfkV1I!8$?B567Q`jB1mU{|1b*}m2q!u= zt#?Ij9iM&8wVx9{!U}z_gCl64>fqR}b(c2q(qeL`uGHLP@GeDWuG8mb`^b*Jb{p8I zFK0tSm)%RH@`{e(d9!zAc)gFV`jVw&*nummqm=A*o&EMF;K+y5tvmZNAbU_QFK%Aj)8h9# zhe1DFp+&Zg7+yGIU4lgG#C@qRhxVbq3W zv_Z-XZKwB~=_7R~0kS2K5>%O@DIhO$3qr6l@Lf0dW@T7365mPj^pBmp`X&3rW7&@GJp<4}^9o^z&Ug8|?&tw+m-vxINknbg z54X^&Rrz7*gOG#)TEj)l>GnPFR2tk|8SGAP(dhEncV9mi`?o0)q3jv*>Lx_|TWp<` znO&eGl@8EsOh>HQMW_!$9b7j>hhdX4FEtE&pjx=8h`t)uRdPj0V>QOu4OIOgB5z4l zNT7fe(2nDTo9AnUF%0dr>Z!-nPx|}lhTH}A{0c+z?04qE| zHZw6Xovocdd|YXRQidM$G#h_uup-mTFFDI|>14|Za(No`*|CA)S11ir@z&OVV$Uxk zg1j5%xvHp$3@v(Nce_QG*vsS*EHV+o*XCXs&pgT3UqXtxB77Y>gfbGbRvCehi{A^6ajOYj4(fIr7dqB)Nc zJAeOc$aqwk;ARV&s-o|-=A53vib(-_Snb=8Za2Lg;uB%Dtw<9pPQgmopw*w}?%VfY z4`~%AcE9#5PN(VI_-ha3^1f>~t{sdmq%9Y847eA5FhZ1;;&r*>T!6wI2us9Gw5b%i z79J1Z%i1i@%5tDg61Ka-Ca6t#ymlTNbO+=n;BIZbYJ1K&ny65dYhxdi$0;oouN0j^ z%VK!6xafH!=TdPM_;oK8KR`h&`rxVOa|d>s{q#@gyE6_T`Jo1PRyF&p-5RJd0`^jT z0r7XF#AQj(M;+0($(zey786^PwGr6{^+pHAlJcuVr-Zzdrgb-R)Ip8E9Kr>cuSC5> z@K58Ie5n%8xe*W0>{P_Tpwy3v?A-3F6HXY$djA zKQUFTwV}csMHH3rh=+y|w2NA(g;W->uh4J3J)CgfC2u}b+#>TVJEIkSqj(W?d%44R zlIJqZ_i1L|g$wTep#JXDR`YZv@XeKfdG^he0s;`1Gy;~?fp^A7Xw|xpR1;k*=@kZ{ z&p%!N5ey2xZtAIIzXro&BsgjFUpbEsQy5&3IGcmb=3Eq8-8uHrp{Oj>nHu|O)48^O zX3RrpA@Pyw;SmLOCIad!f4X&pdMRO9hj5%@tZzL5vQUZ}5rbEwfodH0j;r`6!dP_uihwUp;8H_+2)dU!J@^j>7xu zHQ{6w2AdcWL#zMnpk$5}AjLbbqI;1KSU!}v{GUo#VVV!6{S(}me0fH6qUTE(Cih*_ zb59bgm7Ou;?r!?(<(ul_GayEgZb!Nf0VyMx0#g+RN$&J$_`n?~Ahv?>b}KZI<-6Cu z5#nTNBkmX$NxxP^&8w}SnXQns4IISUlI^BxE^%O)7a2T(!ZzUl)xWePz2&|k2bb3g zxNZfQ?r0x9yzd1VT6RyDVi&(Eq>dKqzxiLEK9S7>;c684jKB;oNpb{Q;-#O1hD=B7 zrreAD-S0tcm&d5l19O2CGWEOf;(VwM4NCZdc06xG@ymbK&mr8LsLo95yMMCglA2hB z^NAyGGE+JnzAocR!mYiV(chr&rAR5)^lY41yXI*L&%R5|Hu7*y2v;x4^LqXWD?8EF zIPAj$)l_>DKJk$G5Fl~4WBLPjn7c!bpg}8jx3TS!%$|9UxzTBsq^hJ{t5Jxvd1XM? zpyd05|KJJP{j4r#KuCgGi<`X%?tPMQDA^l?3DL54#rrg_PcPPGS@m|W*<{HOwV=>YNuiOMPnY0#bmPD7q+?p%PK+p7k zw#?@rps9?*5ZY8YJ`wYq4ef?CyUsT{oDDE9jD zB}wEW3{0B?#`4|PLUv+hFxcncg+f$=xCH}$&@x5wti>L{5s&g59PHLFT&HbF#(xNF zqMkISEI@G72p1{!R9UEcSpq-Rj6jt__H&`ZMl=!@yXQZe#QAy3K6{^{**V40EBC=| zG&cukq@)%eqwD_fQ1cU^a#23cGpeXCK&D8q`KCUH}G_HW=MQRLd@vzFSsrz z`T)LMt)%AHu|xBsfj}F}>3d=*FMlEG-3;WINV`5J`=C0GxvSYFZRARjT9!vNkhv+ zKfn9gn;P4B3D(ryz4{mtc?R(mdkV5#O5C&;yl^EdIqkPHbSjB27|~(q>hC(Xs(B!k zgn`0L*nYp?d17Z#jItcXWMOAJ(J^0m3Ary8=#^_P;_MP)n0>%#p~@6t@cNn1j!Fd! z(upu1H7kDd8sj)x9EhFWtOQQ2qT|76N!RuJL(;ol0B zWQzg2&MBfJb4$_D@zlRrfh32R;iXPU{q!r89Tj1f z1xto9mnIzI%PBiz!3Gtx_^}>06R$!Uld~?)J(hxNm{r5|TX7|6Y+*lbq7g|x87eBz z4L{_Y-eD-?`&<5~G;Wbx9r1Zij%D-F^WV3E98_dD^08PW#69PfR60qYj7*u_Je)Fk z$o&d%wepBY+_mow-ai`VVZ`c?NS;?=o!uGPE`RgFR=pv_Ot3CPetw&8of;$&KtuoH zAO_d_2rm=Z*zXA|$H&vsf1nT+%b87XFNcpcFe$PfIpohQByr$hK^sN9`-~{5}K!XodY&sm%^fUl+}F*CwN~5>tz3^HZ{N zGIlwT@S18gfgF8@m=cz7GcTD*RXNQqA~G@?28QqBu;{e$sA*_V#H~!-m|(q$g=t{s z%{D9zxFcRY&Xt~Cn)KAV?Q2MU7fI}aq* z0`-kLYu(?@^8~a2DyoM88o{G|)_Ho2OvbY!sEwSxFPEz$>j9g=J_%({R~Cl>^=ob{q}NBJZ!mcocKlBJJ$%5*G* z2;qeZeN)Dn;TrKz9$C7I=Z-tP(I^6`dt*#Je#@=NbX*-ggBIii>1n)7E@!>LFCUTE zwj+YoQqz@ttkykNZq9C@+YOq&ZO4c1QYd{?8gCco{#3b*1!Xxoe}D9E z%WFgHy_et~wtmlOi2wv?0#Tr;Y6O{XM*A9qr_8Xoub_RIIkOh_7bmO2atSiUQbh(B z8uf{BM`RWW5E*tmi3bKAy1W2jOafYa$GP!ccHs1rVG6ATM~Sws?#40fro~}+Q0O1} zRCSn4mOF1K^t};%H8Z4DjjFB+YyW4|R25n7amhYYwozZo zE&Ql6z9s`YCP<&WO8unr^=PDAR%?{N31qJN_PFJY!PChzZ9hf6M878TkB#a$agU4i zJZA=>cdk?jqc7s2YB(H)DRK@g&Z?)|6{88|y0TE8zp&F^p@Pw*X|rYjYDXa|y^)JY z-(40mh>(Sa=|=+jp4Hr2w@4nW`>%&<2C#PK6DpxAlL1ZoG;VkH8~y60y)+-v*eC;O z3JXfQ$pmM+PF0BUz~=cHuFnHGYdQjvs8)Cr20lLX%K@`WkOTuvQojW+xel6EVJow< z=;mv+*%Kna3KQsEo%(5V`2@xJ~p*OkytiX)08J<^r!lr88 zLzmRwe+FA5hW)ADw9604Ydl(iP$DJW=)mT0O$24dpuwYh^K~y3fX7kk&N&RgydJe@7r#{#AnfNY$&*_@HOdF z82%f#g6NmLk}qE-OXt_^mo(gTwt#EzDzV1?>Oh-XAp@g1iuR}~k{Di3!P&`nP#Gpi zAY6ar!gk{jy1v4#(D_X2tFZQw8~G#{%yNL52)M0IzqVZWFPvu{uY9_PxY*6wYDReS z68_Cw6uCdoPR~CepCF`ju(Ra_$my#jwKv-bqdE%#*SP_ zA}Tg*h&}-4{X~u%d(O<2YKn$Pesj}v+nF!eRX>=@d~ztKQG_kfA3Am%}*YfW_*Kl%_ty$?`tl^MfR1J^F+Tij5{ZfI$`*hzW&T$UBz!c zD``GMU@beSlnWDnfH35H0aqs4Lm@7FDcf=?g>A?UiGA*ifC)(_^Q}dq$aoei zfjlAc)(?vTza7g@uoxH4g|M%oq<`aqzb92bC9#Xdwjlx=n(fmW)p+cMfraUCkel>- zU>KPfId=}hC)#>|8Q%=8{~?@PBRdF}K$!Th7usM6i52`P1KYcFXIHZaz6k)8up)k= zDws)=I(U}A{J{FE7HVTXcP20@$S3?n~p=`(0nzkjqrg)74in2TriE zJBD7_o$n+ycW!hd+_MpViHuZX#J7MX?#E(cM&xo7jDU{>nIcHJnP3c-rvTB}gY9qx zLzGA%iz?TtL&EGBEFyX>FUKB!!AGC9q_87C4|&Z`y?p+oc#^xKwwJWbV_RYAz76{f zM0c1ELm#;Zse2+xv+Dl_rpsit+pO_ipm%lYW-hWnJL4531mkk6`R0#DLPH3lYb_c@ zF$($Fv8^fv6I+m*=HZ#)p-U;j3Q^cId3(UfJ7M6BJ6{b{s$z66l!Jd!Mt;5f zC=d1Fh_^@^J0O9DeGtstUi`W|xYXovdk<8Bsj$ly*Smd#ZSVl~gMr+N1h=YqB^46Dn4Etwr_9%lmnxlfY?7}l` z!J=KndVC)*A#t;!_x02dWRwL6U3}PJBgz{}UthRjpF5Wd;eeLr%o&WxBhdEC&r~mV zOZ9Htd*c^>h+e`8c@XUxi{26j(%-Qw(6Rs9bW~$117i6sC6I`69+2PCztjS=(Z9YL zK55#W6xtGa^*swxwugWvTbW3*^g(x{%rMJ79&A(z=7Rd0iT_Zv;yZlgX0 zkIaDzI+e5G-Rmsro37HfV<Y7LXA^WX3R`_t!B`=#5>_s82q*Nq{Ez!F{i4gdgwiXSVU zGPNHuGIUzIyLqnut{+o*JAd2~AXC7x3p}{ryVk_>d*0*%nM3w>Utm@Q-UpCe5A>kG z#{t-%AddbiTa*jB0$XEhvB&cf5)fTHZ1#~i%=td+`?iqXD{f0&&lj=n3s9P5g@guxO4Y!IdB&T9ep2M5c!l2Zf$6FCD`~X?99n zIg_qW{{cCa{klAt!y_iwNd_3w5BpqI@?VU6AQkXV`*!OVytNGsvTW=u!c*1d>LXmj z><}q>;T;$YFDPXn9E}}=P<^%fGA$s^BeTQ!4{k?)7#U<@8h&`ekN zy!-W~^Q`zFW1ZF8l%U}5YmrMb->~4R6l;3IS_Z;rAFsunax>`Q)p&a&CKgw`hBQXY zzIj!S5%J8M7jb;pJSdQQjnN+hzc)Y)Dl@$7? zN$wun143@{U-_hMgHEPTTEf>Q_j!GNj?e_uw)~UoP=NW5`^hiI?>oQohux1JBOw&oghi{bX(NF17|N# zwebu6zLcN+!r~fZWSWbrz}3N^_F!eCpDH?An&)+5rD{|GOqw)F{pEJcg1{~RBO`+v zSceDAlGNEck5hBlgM?+^QwOPm=o-wG0PA7XF&Q{eM=jWYPg;QlHbA9ODLBD4xG@pD z%e(*4yVUO7bl$Y<%JM^F;|~Q{!^nupt-&!4qvjj-HbMxGtRL1;F@p)ig#wzPO;$WN zq)odwC;`%x^W#7qBZq^e5jAo!!^qX%*P6584^$}QbGT5iM!VJYE(xq3zQ2Nylz6S1 ztI@CM`fy!R=te|=oTq(#8Opi*aF*nD_l@ReX@S(r$WJ7f@Gs+8GRCcSBCrp2S4e4V zIGcQvQtb3e6w8+pf9n&uz3QRQ;#s;sW>1=^oAkCP3u&U`5x+v4(`Kd>vu5ev$K@+d zfWPmZvY96&Cd%679vB?1EGzRJx32zW#6bG6&Zc~8R-}(7&De3PKb(g%;z0UjIgQ|2 z>ZOX}ZwGc85>D$@V11=AB!0A&^L1lve+E+fo-Ce%nSh2@>9nl?r0ljkhT4f;0LBOu z6Px`lmv(hW22j)}&hC@v3YAEVxkLjxIgDL$i}`y21u+9G(%DQ=fg2|wEqxxxu8A| zzU%UL8+zvkU-Y*Y3DGCj0w%n;VpYLQpop6!wm{zB(c61{6l`3q`5?`WhNenK+>o~{e;A7S4loYHKXpXPQyxC*Di}i9%6!=n1mNNwW%#4opIc;YB zZL}FRisUl+de+t`BisdKl*QKsE%(NQ)>;Tx4e}pRHBxm#YwC^?u_|p+fwxR$*??-{ z|Fr<$!87IntRZKec#151d?c7-Zx`VZPbEas1r7AMQssep7Kpq6(Dw=111|n-D?jko zgS{+J)A65oa@DOiWPX~C5b9s8+tgT`j+UiEs%(QY4E{tnEK!6R06`znI`r*)Cl~0j zz7B`?MN^O(gzLc$q~ZBEYJdNrN~49wX||uncZUH6%c5C0E*N;Yh6^WOvzdmuV6P+% zkjtuRH_nweTS)*cW(SzV4|bIGdEMhr<7jwX?%4AqUzr29NWtwb?Oy(9D7i!|IgZ2_ z`m{9stz7Wh3I!jRjq(1~;Q@#^T?7z{Cx}9$4GBq&?I-6)cQ&JUE%kpp!OZbe2Xop* znVxWp({_N5$dENOS}u%ca)3rJ^VDt2J8mk1YPa5ij*LRNIezPKu^|cQZb0bT@{pMH&6u<-KA6l<)vEyG?M37v47 zopIThOhMgBus`sKv*KO2{3!kWSFr`-{<&}n-C;OpNofJ!YmyQ8sDDc%kifDR6QuIv z*!+2(Xl8{X2EKxcCbZew$C;Z47jHT`KAqh@RBQ>QQ+n@MmkrYhj7ZuUuXjgSFVx0x zEjl1a?z0KK???uq8t*lP9?w#yR+IE#7bc;D^h-y6NFXu+V`J0b zZTZg@;hV{)f(sOh_4_4H+BXpKq*9Wjxa6Gk)0)4~{#ZUUp++n+Kj#gF zt*yHGG4;=kTx9K8AdPj~ElfiY%J3(2<;R0W^GF_oY z{N5}QcMj8ItgJPiGGDO7I!BeRd#cpj`q90lX*bWJ`MAE8lD0_g_rTz4{ktH~4Bc`m z>eRPDtn{A9ah8#Os;(0_Bf;_kWte|BtfW+pe$wn@9_clk)xD-8%Dbd9pWM$a^X{j& z6$bOwAnl6r-8+z_{RCeF#v1zRezVH{A(1pP>A6zIxck~!YYBzcl@*F7XzbTzFD~Hi zAqC_QpzQ)q$3DsCfguvuk9{mCLq^K;d@2hVUp%_b_hBw}S^33bm;vRDuwb#&{P*0v zye%Z&RDI!L9!TU%Zn#hQ@Vm`8Wh7IZ{D~R&`f5;`n7|y!+T$%>?z7l3m7!%hvLF3- zyuJ*`8ua4S36ybzDvSv;e?Q4iUy3^%yOAqY5{1hi-;b6xIu42lT)rW!3LD%sKr^S< zz2nbZ->cToclNzFPvU_d4}c3Es6RP-3O>H@^=wY&j5!27F!*|(AUjVnQhdja zalp)dqUm#~>3Q9-3>Ze>2ljoS`<^SUsYJuCOzR0oU|n+2&rUpl6d|VXjd|U{e!T{b zoIPVu3JFHZ1Veqby;Q-{4}5xiNZL=vvS|Ns9ixk!>7Dp;!)$@!hY3q?5`oFjOf?xQ zY{Q6D5e`LT3<+X`P3Y!R(52;d@h&H$=|5V*Q$~;#2Y|Il?g#%n-L!m*mo!GysCK`X zXR~|3Q}n5${*nb?pux{nThF#<2zZn+hJk|)gkx;5BzGWxZCL(QJs=?|dKObFvZ+~=0QWOUUgY5D;_}{p-?(&(<+i!z%92(|JM&#f^}XhGz*URD zN!osHpF~_YO^B>W9flSbJUih}1wSMnT{f8-oAv$H;1?=>@L+-Ctg)>ZyZnfuYfuzXkG2$=W6JNR-DA6EZd3MG3>kmrnpCg^Ofis?B2kvo@Xo-BOY)G3z5A}S%fFb==gM3AkwPxw`s7|HJ zz6PlDpGZdllK_xm5_J6MmGwMt&Fj_#2SINk`5Ert%_{ru zF86Ptcv0)xutXB?GoO>tnz&V6oB|Evlpu+ghvC2=vp(Ubz4Ar_4o$|DPz=f+iAnA}p5FS3zRIv*md%9jt-qx>jE+sffcVj|Qu^hY z+u5QfAE7cAl6)cuF(z9w20+gMaEsHy3@dOufaB(S_vy}G=JSSLA8C>wjI9?J?88DZ zet^MltN63?zG?~xm%t-4%nx;`K7wUFj0nKsrvI(LS6=?S;Dr&BL#I`ul?C~byf_P- zHny=V^||Qo$OE*?8hN)BBAhmIU-G!EahuchICzOWmCgaelMb^)U(&vIJT~_sew$s zc3O^h&!`bjB41k1R{R;9DsjrG98P!DnXrW-QfpB74aFH-XQ07n7fZg*u~{t< z7L${>>on6l7_Uh`D})X2XY*Jy8=2GOZ@y$m66^D6qgBTGlFHB1?}ktM*Mz(|DJo*? zt77V5iz7^*?2+wk1BqZO_idX2?tT$JKN9Xm)OSxA4{hw*t_H5i3AhgVW4V3KWi$5i z5gN6K2c^>gtb(8_zq`8wjx7DFpoK)3+sQ-UcH$>DQH9UBv#|q&RyFe$DH6QtW>#y@ zhduYON0#nOS^sdOUJiQMywSX)cR3(pV`2Y-CB9Fi8(P&|vg(N>7*v%*irombZ++4 z8Ae;VrRN*Tcmr~Z9xq7HgZwp>UEDXlymDi6*SW!?2A)D{jk3AdiCtyjmj+0czEfHL z`#PabJs~G#MUqkVuB7T`)j9AuxFb8YSrDeZ>U_riAq8<^T^d@OM2kc-pcB)?hiS{6 zwv3hu6IQ+A{Bg+r%uc{sBc=s+H9b?ofcf#zyBlNd&V$S^@_=`87?Y89-l(=fhEo0%7^*IP)>^zft zWY1kKT=Krdu)XsSbA^(v35|Ow{*@uM2i;dSf_YqL$qoWs%E)iVRipS}e$Ii7W-&XA zZ3{&MjSoy2Jg1c7Y>%;duBK9rwazSTB zh%%>|3B|=8r$>m$&0IRJW^#iC2Jt>zC$*sc5@LV;Me&wyGa0?qz64=?&*mY(;E}xn zb}zL4+2H0QxZA3Jh=y=oX?l@6VZAuy(!9ZYx(y*x69k zGS3vL*<0|wCOgm{Kk-(o_ z$59BHT*ObVt}ZagmpNSd_itwybF*-<#(Cno>O6?0JRLyIxcO%;O&xcEg)z?87^N3t zw>6{pYRhe%BB?O-6*_`#;liqa7y12jIf3CFswhJ>z*XGAIlgbb2NamqjlQ@vZijaD zrKBXV{&jwr_WJp^XgO+dD|JW=WZy+E8Y=BG*l;2iCA9nT+L6@E_`lXfBvv0(pR zJ%$W8ceqhKO`nI^<(V-kh9&dI9VTH6r2*ElIR38uktO?$z%JwfR(VRDdZ~g!rEkGe zK!NZhkL3ER?_cT>$`Vx^udw+PZ{>Ea+LZ#uNB=m-K^P$;F7c=Yy=l*%+XK(le zE^edySP35TsD`1YIx&L<(|V%K@>|B5rOR_qXIQ%D>9u!cSoTX0uLgj9zea!OKVb7x z87)hCzeDsbEFAl9PNr{VESPzXktsygTW#a4Cpql`3Y*D2%kH7f)q>JTvYt%uKp-C< zcDz`*zwK5FPppF04Oo_OnUrf5MgP(J`S7nY&HI$~UAjj?bF(11=ZOfp`_5}HjEfSy zqcyl`f648zhLC3oDCpo6%uwc(k5kC+0OMdF0{|WziolcKo|Ze0>xnq+<6_`819ZUu z+G^0D=)XeNn-&NbhH4C&#aUJDRS)WqY6gnzl_w?czv$I5|$o1D~>b+QF1o)c_n*IDDPOI90%-c~L;52!wW$cPPPvXd8!@dU) z-m%}lDgWDaQ)9eiBR(xz?Pg}Ag@S9%+ zDURCK>>eKKP0CVO6A<$=JHCosAF--xO#X8~O-m-!(ym9`4+(8~_#Hq!+OQ}Q+F$$!%veFYy;I*z&+(qn|Mb03u%&YGESUQ=Kns*)(f)X1cQ+^ zx)}XHdjDQkm)j?$yu9*nih8U=P@5+j$KKW&FV>7>S%-)+f`feSWWoC{tDnAiJ==Bu z@|dpowj{`*e(p2BwE*moYw>V{@zdadZWI5)2yMuE-THM4#S1#^w)`@0hA$2p zzO}%@mQCE;l9!g2)Uje=wGZvW1$Rl^(!`#-IDhAD^}8#)Tq5{ zp+Ng0I^tkI9DgxN5w>KpadwOjt*+WX(@Q$uX2fLpMUyUdTxcJs;6hO>w3Uvz@t^xU zWBjsPb%&r~!5e?;UaOUa!54jpAevL|cinZ2AvR#f@?o{7zD!pLIsL{8f;v7?({wAP zVI_-&XEcY;A0YeL@M(6gBCgDoQ2ms6VV&K#Fz{A|YXQN!alvjzj-NZ=%jv+bYw?PY0}<+=8qAe4n5S7V$}UeCgYr$R9i0Q<4F7Fb^k7JZ z(7BS`bRa^4jAOizqgd*1`}aAz5?vsHkPc^yzi>VBh~bh8yptab;@9>Ondo;o@{`j^ za-;i8e%*J_-%1DaK=w!P_*_358X0B8(HIyTJBBM1Atfp(@+$Brz9#|fu#PM4!p&`kk2+PA6*re z`Xg>--xTXupgPa{<(vO%JimIixy9EqaIPq{HRv6%-N&pDckvb6QcAR7{~=IK!N}0t z62a2(%9-7IKCh)E6FiEbXBbzF1D6{3L_YAQQ$PP!W~vC}9nv>k`@$)Z$OFSh}2 zbu%Tu%jEsxm-+c5Im>vSdk7dKgXERbbEaS?TOw&d{qI2Mcmh5X7wt4sqbisd9D zgjPI+ec+lJOw4nQPylNrKSLDCA7O?TMlDbHOi?iT%VzII)epi#t<$Z7z zM`qDn)~Sj&U^Z1#+K;2HMf}i8=KP50>mPvfC%vdK(WxwBy)57mwa0o1ovL=@ zBlew)KBRN$c>Pkd*UCS+<~P<)#m}P|>2CN4)4l6HL48G8y8+w2d zjf2qzDJa%{B-aOy7L3NpkB%qN5GxC#Soe2VaGBp|e!%%g&mGx!abp?#4da{X)>*!> z3-KIDgl#vk)G~6x!wN5n18w^h=7%@W9U@}%sb1*a^C$4q_h2u5piT$jr0$el(&r#b zC(1{5)d0M*;d#!a8(**aOvi3xV4HQQX778ot23FRE8UXaw+%GA$KXJBzQRjck=VRm zTXze(c0jH+n!hR}Y-Hbykm$I_I0^LV@9%%YMVC%%CVWV|$0EKrOh>wH8Q=X=2tTsx zyyMJUgwf)o=M$5sQ50k|e^9?l9iNq2FkF1=)cks83`4Dq`T&?&za;U*{oXu+5Y(sEo<^VEr^7JsK8PqUE}MvBd3}3(mLS&M7WtG{dZo*My@M2Yb~!sc!uucM5WDHGoA` zrO+hG=8xy7(19ldBVfij(y;wGEGfzz(H4ZAF ziKNjntSw@mOYHQo28<5ySbN|-DdFwF8yWfW z1k5Rvi!}raKX&)O5I_Bk6f2lish^C^?6gf4gOVWAwJ$;YDkTp$tV>D#h5UG^M0Sz> ze6>6P*N$4aihJ)}*1x1aRlOLil?^d5GwbdjwPPp#t5dAr<-Ngk1;a9Gkv+Li*uPCr zU_lm!=H;rO_E+4rhQM$SvtJp_3EeNF!da8zBGTK0ByQqFiAn^>^dI8fXo&EmonGUUyuX&i~4xg>|!+A!$Y1z%(HPf^mtp2$;7xjc0epUM!6z*#!mcP%> zX5wvQCP^N-hB9Sp`DPmPHiZV!YKw7}dh9RN@KM8^ZYLjF{#~O4wcPg$>3?$0y?KWM z&Zm04t&Z!e-Bw6^BJaYvCgz9(J^#3`_4 zP({n+0!%l!Qlk4z`BPfJSZ)^0`_Pv(Q%(Ky2eKO}*t|RQl7Z;!`jS#K4%cnQPB@eb zKIk&r4mX(D8Tw^u6p81mh)laIJuY2W!aSfnnQ0RW?L?JA(eplY zV)-+#a{D(3g24+McNPhjXYEWERMo`j)dqg1U+wb3GW{G6;6VzfNXp&)z%R4n1);jY^Mo$h0s;U87* zz$g#QJKiNxf%?zk0l2PEv^l zVKB`|oR#-3Y44t=ce+T;a%1WU`wqp;`hDBw2FEADC)d7JmCT#xt8K@(EZ4A3yEDzW z4>LnkgSzu~06flw_pkngol0~R`WmjEWkY7uEG{nAV+>kVu4PmS`p}TkBPldOaH=EH zq>O{5Mt8F_NS6O}tjKdS_r2~9235JYGeR@{#<@dwUTfzMtIQ9~7cVO;bkZ7*o0FKm z95Sr3rC-N6B6k;UkRm?IqD9FW)fCn(VR#+*42-^BBNT8C$ffC6#-w29>Xjz#wd}LD zAWa{9u9BzwUX&oKMV5X^3b9Z3EtPvydwHXH;??5%16brr8*eVSMqJX>=&P*!SGe>O zKaE;*0s_9Vv2i;dA|T!abOC^;f(r!r|8dtUPD*C2-H|+io(2l_1{jt)-AFm=0Lv5* zHsiQAK?jJ;O+y|kX*-z$UwO$A6B9vc51vyjQtnphwx7~gfKB?#j`8Hut#?r7;unj0-i`+qG! zp2v^h^#hk0U5Qf=*DxL>TOPv14_0rv{lu^tN*>U5~_zNV@pFhFE_3AIlI zgqn_hghC@Q+WER+5g&?7SU5TQ8ItmD#?JFyFS^DFBR>cxtOU2M$$Zlnv7_$d8rEBh zYPq3>9dx{UlP59T^qKkV;^UVL&j;MKlwg7|!_-tKI0S~sd`f<*V?ktlGB}tMz7i!a zLiBg^#4YAnxuwtwX}Bn%OY0(29O2sawbJ$)WlZ5{moSNJ+tR0o zs0kv$@WGT0*S7Ig0@%$J1#jONJgmKRyWLC@aA|RV!<&vZXf|&0Y;5;$($>qcE+B?_ zlddp6JuO)|o1^;<06swOIanmx@f47pyu91}(!A;Pt;|01fyqD8e|eMzqr)i>q&sRI ztd1~!Juf-FC#*9$-V_t)HXSTZt25hO2DPVaHa`Tl=Nk5JT?tWbZe(X>V+}2cdtkce zSKiqj6%ng6XOA{#%A2sqjGsN3uvb&3*cu^l?MOK*_n+0V4+w~798mN2IC=#pDpvUMMH_H}>GS*}oos<#U?4;ADu6-T24}v2EV9-P+zF zY_Sh+Hg;46Z$ZwfCXT{MJw$6zg6_K(-HV-{V!v`?Ev+uu(4E#XFbj> zp3I=ik3V4}EiL;@Ci3J`57VfIQ zIzITl+7vy|P^rz?dUu$|BWc5J3kdLirrH1HncuBa*KW80&S0e@?Y^>vWgyq1S zWs`;9z8Aw^Rd3Si*OI*%xAD*Nx7CI#<-lwRi0JssXeRqYK{svVI8&fc(|}vPYLYrG z6p&-13dQWx5*odFwnh02;ppVWxr`q|T5g|n9S|8WpFm=y;v0_Zqh#t7vRUOhH#x7$ z8+hT2gN~5DYG79%;(RNdNfc3wpcF$8=Wdm``UWp*_6x<*a6yDO-QUX6-$E=m-)A~+ zxbL!AFaY$cQ*pbtsgn?Ar_R-ij(0?jI-io^QC}F<7LRZRgpM2SwHI}21b&5H)Rdjm zTDnPpp5AvE#s#V$&CavON%)s9UpkQ1$d#m}Gcz+k@w<JF$~BBmZ6a58Tw?#Q$j5lwi^mS^o7&EW0#_2V%3&hlP%VsB`Qq z#&@Ho-e_0~nkM=(@^6$b(TE2cO+NTI zq1uQK3wJ(`D}812maXNyfcbo1=nA{<)0nA~8^Ew(k^5K&Ys|kcp>t04L-E9?!x9~( z-MFW_z+@8=kf>~ytT~Hy5EM)UDk1G_<(gp; z!L>Oer%>y zFlJLTpMf(v%n0rjfR$L!vm zoGmG4D=xOntPVz^xZ&NWwHE6DP}6G&UY|Z3Njf>`d;v=)0~Wf_ooG@xh(-4AiT= z+vi1T1z=urNedbQeO$^87QkT#9ffMK#;8j=KqrQLZ9Y#2!doDSaob8Z=a&@8<3wA{ zV<2zYRCq4XptMFbwu><5z!U;cC1rHJWUKv@|*Ic zJOM^fN^TaOq=8C7m1d^XIQVv67oZUbcU0ImRKlt#A#KxqE|7YvU&2k8lZhWR@bKe) zCN3`+`Tnu`HPKye6K2gBR#=z;zfyYewp?3-!a8^pX3bRZh&&xxZB(Xab8-_#bGz_9 z-}PwiO`Z{sW;TXtY{{v!=ifu>8aqC$K8rw6{%)VUF;x&I8ul@YTtb<0!F|4AB$XPB zO{BZCGw@cv-?vsTI{y_NX8F*8EVYgHyKsvrRIVh%*(mTfxe}kv((d88%45r#@QPlX z@49}2KoB{GQ-*BPko`$(gqS9AdtjSS1afK9pc(efY_XxcR?V!}%;mdd%mRz$0fCno zJwb~Gjs$bN&MYY$VxAuihCj>)1_<~=vy2>rD8uz?{hO0|4qv_Z-?agCF}Pwzvn8Lh zAw^O2-FM$AWbpR^DJI}M6-_ouhvZCbp7s}dfk7(hP{83P_2&7KTO6G)80Zx((eOR+ zM6O3TISM_=Ye2`myK8CNaL5S?55PR5`Th$htP6yq0%WKPeS(&b&e}}5$-|D~1N&C< z^~r;7d?9Y`h8Uq;GI#B?Mb9_mnP)VshFu2<8q;WI@@>DZ4?O21`9zqgMU-_()a~q| z7^`g7A+*otI2rd@;)ASpqemCm+U%wZeY+5T;FVI6_U}6#eudP(xQsbb_43}6UE0{+ zMQO*uhdVMnFCz;n-*po_->7_e>~l&>Nu$}D*#N8W-8aUch~~?=$nG(ZddkVr%2Dvt z(_!01il=tL!L(-Mzpc`!ByIClQp7_EY~LEzoM$Sjq1}2*T#@IE2ptq$1(c8KdDt3y zdnZQDim0mNphd2;K}xKJ;lzKI&Gm|9(|ZYcg)1tsaPhVE9>HhzFbCGQDxX@2 zdR6URgU{-gj?A}uJB?{;Le(wk3KUAQjhRgS?Y4=b`J_4|zZc)wfBc=#F_Wqp`o_cc zK0v6qe^}@PnEJRc-lvixb~!@J#rAL|BqUr{{E(j{drx1*liAqysBRm4=EmOM8DM_^ z3YAV*$9w28o$R`djUPTse_3A3odIRc$#(>xwXxG4XXo*_Vg#KiKTw7pEVpDj^G1Bq z20%*J0{;pP(Bv3d8YLbmMTEvl*nJobl;{_wFl=0*U4)|{oGUw_k+`vW14z4 z@^dx@$@BTvcQz3?tw`cAxNy07@|B0daNkjbRK}OiM8k^24gN%w+eiLz=UE(9gdrox zlqHYpbosU;RgtYj=h`Z4y0-{!?4tMwms@V=t5PCcjabs(d_PQ_a#$#Qn3g@*;y$WI zQXw{Q{}i6R?3$A4c_L_R6~E;78jGwxxiUZVZJ}?d*zWFpS4NYhbZ$lG-%LWG2EnUR z)Nw_7H}~VgpXM@2O~70jJ+r zhTXxYgh22ex$cVTnKQSGLU#7F?N92gyjAl0UzfKK0S^99g??K;nyV41FF8<> zm^Ikr&GE0d(D*hz55grUU-$szZ=XCI0>3Nay{UNoVHK(Irawp~URxlTOnQWc%T`Se z9efpPYCmhpWaQ`Nm5;B+?V8wI5B+|x4c!#kgoIPmcI)kJ4iX}TO1|!4b8py1wegSF zoAKDUggo>kJNZ9ySrev~t&{2sSh^0U7OA*2P zE{_(U(qHIyY@PHK`6_fi$``)jipgy z(b<@+1*hqlrS-hSKKxH#C8hZOI|!G+h`cq!M%YYs$3>_1R~)9R5?4(qiQQf($);kW#EY8@tVupH~^dYcG@BADrFyn~~BOI;B?#~&`1 zJ7q5gyNhBdx}O!G&I3g*UvdtN=fq0 zY@%{4rS!RWaX+Lt8Lq2C+Y=9~1$B8=4p;UZ96e)&ZWg9p&o2^gm$fENf`V}rEJOn8vUiAdm^{8vx+-~ zpr@j^f7ka9cg9);HH0J-#ogl$;Kb!3Gbs~1Nmy-6;Erv(z23j}jiB6lbF;k6*gjOW z&@3crpsrqnu3sr*r(@Jucl8Bx{xHh1j5bspaWb*PqA$-z;s?1~^fmv5vf%Z$&&~_P z*skC=>POS~hzVz4&)Qpag zcK}&%l)x3?Q}!zex>{e@)ug@fjP+YY=7B~c*BwqBZ`Ey)Q`0Y2vdED*I!qHJSAR~$ zK!3jYiRmL+jKO`CfyeYKejEOgx{OB=PLz2Ef!x(UqWpUr*B`BTe}9g@ak4i_^`a9I zeZkIKHt}~_*PQRy&j~`?^iC~OV5g4-t%J*C?8|zgH#ivBfkEg~#^{_XC@5c4;JDD{ z9d6FfX>KkRJtJQZ%STVL$68IK!|@GqbX{}7Ykp(4(8gk#|7DxJP*)T}A`uK1T1X$$ zyJ=+yg?T$WK}dUEC;wCZ%wCZ6QtFNBZ{^8PT%9r!eo704zHA>1yIOx+g+tRP`LFH?TF!&)RNiUsN?V{H%FTN(+E#_)%=jA3 zPhiC9Qg(l+S?KjRYW$Y{p()Ej8%(aOn&!7bOK%6m)fXritJ5|9>4RfWMmCEl%>7Sh z*KpMm!PF!<%WpAF&?7i`*CAN)s)R2Lo3ErQ5&d3JM8@a6RzKX*@ACtpZRqvGwZlg{ z|GHQw0>{wUyJP9r=0<;i!+P8I`d_5Pf;8XkeU7s{@;KqTX?A{%ySr0@y9W=RyWhQY^D8r%$;@sxyRSUw93CDX zzzsrf!3|iXa7)2CRkJ+<;eKlo;r*=t97o9=HA)EJiL=0!==R;fHuTx#$a!fB zWGF$5#7UKu#*-0hXc!ZL{ZR!24E0MtWo1T{HEHcmd0^uTuk7mY7@foY)MepO>EwHu zz6JoD7wiHrBMimEB3&YBZUxc1cK9m7z*;c!Z6Mxx{U*H;R=>5}3?Vz{J^c5zfuf4e z-@-!(n3_sFL^(Z=y7by=-+Mps;CQGT(;(}eBrHX#sHx#e^og4%s}iZ@vl4sDTHX1h z;FemYItX;1*4RhMPM%|CxhPhArlC=BRd=kE9Y#uLC(7>e8^&ThbhMvBA0SZ?Yu}+w z9d?9fN)7ZpVR+uecl4B2WEg69vh07lixwd##cYv(DY(!EV5@DcAkB5OmQ5|DGV|5iI=rc=Nqh9WR3?9_vNsLx z-tO*gOUFIYX_oMa9|B5uxzj{&m8gRfhSrye?l%8ZB`!DRBnWKHH;2g~2rl6q>?66? ztX%ZWi$1((I2GWzdK3Qs3ki50?7liCT_IHS!iCA*5qx`i$$jaQjSvOTFw;af9v&|& z-T<{ffXDEuT&aGK{gMRpd*EDlxuSOvYt0UEZ(`yIp_;Ib%qIJZf&-Hr9i8>s3@1ML ziZxK}n2`8IT2>~OBcPE3u+>)iI5HxTuFVyhDLZ4&NUK)<93|QRMU3H_`i8R#GQ#J= zPg|Q8`tp4X@-OKhD3g2}3fZy_!z8g$oX~`{n4h6)tR{vZolVaknqY-Dyk~I8XhZvr z$nXx|x#x1a#lx#U?`cnWrOQi*l)jw;r8e-ORu0bTmkxVlLEQn&r$r0t**CuL(U;9) zg8pkpsqs1rWF-Yu7%3yY=g^PujP1N9qOA2O zeryUwK_5pftE?T^xA35|NI$R|SSPD-yAWL1&4WG zwmvO#E_-afT6H?cw-Kn`Xspl9Iw2Lq3r0KzcKIcYXcrU~G;XZFEUHTRY(l>e%E1qR zA6~-CWlU{lW`;Xb3$*6SDw$Sm?OR!buNtE(pB^e!>XOae;?ZVs(?yq8Ayi5Y zILd8dn1*u5lP?!QG_07vkqc~b&Hr3OC!%z;NoO0JU_ApA!j6E!L_9>P$*`cIVIg8| z#9W;?^nb0e^5$%+fFQ!WDLrik1xim5lw@AFPX1G8k*26$8yKO1oo`M;5N)QIG8)7U zUp7B3%7mc?5BGq4dkeY8cPskDTU_&}On%)D^UqlW zZ`l6wlt3glj2mQFzD){Mb0Q+^wow1}f2r$-+)d~GQsIvKA#aqsW?Rc0$bRbLXgy%l zqM|0+g}3S<)Nygpwt_0VmdvT?RCsRc<#)^BBWV8f&NhNA zW0CaA-{}|N5&yRB=IS{NBGbfH61K8YXlBNIo2#UPNS$W#CdON4A)j`0D$a}Ce*0RB zD{U1-pGN_EH19TmYos;8^9-v}XLST(eVu1ign0S*{u{i|Mw@8F8ICVKAuEWaCn19O zx**Tn?m=e@oM+km2dG6QKP~k4n`tR&FDmk^(-z8a!>H_YfIY%n)coLegye-NU}Xwa zMeSFJC!f=GuM4Mz-F)+Me8=3=ztt+xohnWWkfRH?QggbCzA_d%e^68N=l2#Z@u0gD z#nB?oX*a+gkVI(7Kv`N3w}Vcs2EKp`d0OQ6J(^_=*BdR}g&RoQ{K6T?1?wN53^_eK z_xP@7L7TSD{DtOEdkl~(axi&*(O{M@oYXzS`ZctUm;6GKjXi(9XXrVbq?a#5saCF3 z|M5OApd09$fKC>5LkHK@4rAsUum3rYjex4dntD;}sb%K^|FQoT)$XD_>B0!xxz^Tn zjV`7~F9#-S<{3 z+`IIO8)EvoDF`ajDFtHvfZ0@wG4@4M2Q*t?x9iicJx-^$a#a4I*#9n2Y{@vNKzFv8 z!cpv&)a2yrXB$00Ne_rf{;MI&bg+#Utm_O`ot+<&c2s)-If&9Fzc^e9!P!H1=VJF( zk%K|cMwH{|bH}9#d3_lQzJ}rTk#A3K4E`09p~K(Rf6s4`ytRV%zr36gXmh1OaI8ov z?aRNuC)mXg5kX#XNxIhQxqT!w@w#CmSa#1@fZnzwG>J8S#$1yBZX}_9D(W4B*Lq@U zFVW~QX9r;nYo$`5w3M!3zRBXg;;(+-`LlzD6)GO?IsKLunfP@OX#}sB)Wo0g2)+97 zlKxKg0VnvJ{n4-JY?jzr1*VJAdYZ5NL9h&cpy}hY^~WE5e3h$=h=)C7&he1W^5D1{!TU#fn3e2>NF!pE zncJp&0ewefEX+8f4};6TOChYQI*IP_XCI-v?i=?tt_(E7v;g<8!1 zKwen6tswXRFe&)booss9+!s&gHsNjFrM7&4k_T9*BD4`+i3fV!wg+G0E^zM?E`K$TlK#Y5K z=p2M0bwM?%Y`i%ev34N}`gr{`4fyx8T@eC${{IASPY>{TWoE!p|BJJ;bD~VrYhMc( zAtb9EaeoPn69~gYbo(2C^w;9XOwY$J-8oU6u!N6Bf#pYly}dSW0Hgz;P66=?VCMuZ zT!F#t=6Pcw9Y8Pt2hu#lvR&o!GytY(&L-6Gh+!(wC}An6FRTr$zbDiV5HJYlXgTei z-|vbyD%rXBmAY;(ZpvTKqrMj-8SN)bEASGE;c%jmKmRU*xzCQJ0&S6Fic<{jog*;ibI9ay+8mQU6jEL!j^Q2gHmWjxfzM-W(WG zQ&}Ja8FdY_E39-n<=>*1sd`SD-s!hio(!e()>a;2_f`wKX}w}tZiI-UgRzZMYreVD z|1oY``*+;A^~Y%Wfw80eYViXQ0dv(0sRsiXO=@bY88B!A$b!;2hU5_dyljP8>XVk~ zM7TrC^e63_|6p6RA>K1=OdUMy*FBzA`$GiNuS@O|M=TurxiViPo*(z-rQGjo47c5m zOe#LTae99I&qv|TH*0m(7kvaq64tXx4kGrcliqX}Ln5qctAX zlgvj1`r`V@7znfB-MZbF8U0p_8s<;MF1wX~Nv=53%1%G1UK_YI*f*Gnqm^Q8+oO!h zS0<5~woSM@QY3*dx7n@qOJrp^%P zox~`u2-UX3Hb-s-vy4_@wv){gq9UyLB&bEavA4)faw{CdC0gJ>!gn8gp|L(2Tj^#c zL&{aA=m2_1BtDI$rM!kY@6EBuK22$ME@m|a(70!SM;2sAzqZ->NYry4x0QMgZY5W! zwEc7x)l*HN!XWn6G!=0)n=)%ZP0DQ3fmTfhOg&1pBFsC6@(@o#6m)cjGx&6C7XQxz z8~`fL~Lm`1%Qb1yc5LBdb;NaZ~%wWcsN(!Ypr! z9*{7o7P1jz0cZ|jw$fG-CbO#$Rgw5lgW>v3O`Z6z;K&o)JUwk_M4eIHruj>!tSnCk z5r;s11vyXNi#YetV(-kZuCB@X4l0Vjg26P+h(nEXA%H&6b+V%(2sadBX$Kp8)lyvb zcp#9xIBgLT=Bs^$vNK@7uZ`$D@F0E+rDcpCik*Om_)pk#^c{t+b07>}QFH+M=sCfECd9|w z&Avll`*xS(LP<$p%^{WJ7LKsVhe?ilARc)p{4W~34pLcwx#iXMwb!3PB;a-q7`A00 z&AVj+C{y{NlIS@(5 z3}Ri-b>{n1$QuXSLh10NYJ)tpi=QV>IG=CIsXek_pDDbvOJnBe=LfYud`F;h?_*G; zqstx3RHi?*tc`zv9*Xq6Z$RKNb7y_fAg=y*T$0-A3IcwWVx3Z8l;Z8<1K=D?@ro5k z*{+pA*2T|G;{t%rHmWc)h`oj|pNRe)A>T4XUo>BQ&YX>rlmGU8aX0;CChFMu$F1zK zNA!iRQzLFWA9T=iT=Jz`x&-@|&ga_hYI_GI4EnNKp3mB9jttg?_GG?7_q;#WeauGC zGK1*-yxUU03!%vGk$$6KmJLi~J9-PAs0H8P!7oOn9#^QL{F`lSoKbZ7TfxCA@(ZD- zxu|#lyrBp6YFay~`Za$%6f; zQby{BssC*qABL^Snr#^oX;MtZleLR5^M?nEV0R(9L$#1!m4 z`f-vIe%Z@{h&edN29mrtgmHmEdfxQT-=`MHzIiR&FQ!W0gQ}hp)D>!>r@GwZgMcGO zwIDax8UEKyz>OXXJ@PXt9yc*Iwgl$EDl_8D^nga=sF_SY#!ec)I#3B$mt7ERLB&?dcQ(;>_et+f)clQ_d#;Dg_kuD7o)(08v2Es?r{nt`!pDRKI(h%^F+8auKY}P zlNMkN=|0>AXK&2KbN^<=^7HdM<%@!nR03iGV1HjNCm{joPl5Uv_yD*{feVw&`^@$; z^}xydqLMdleoyQq*ktO&edb(2xkw#73-%3ci64!b1I);E4SRlWy1%z;fKoSZ;18cR z@VgtlTiGRbO{Z(e4ivLg)}^`E!WB{BNzoAQ_(l*3rptRB@@}-m{2)=AO+aP`lEUxE zxhZv*1)&rBgy?s4vX70jW+<=KPY?2WDMq~})V=Zn*X|oLtWdTGrx{||LMqX*>K`=n zo%iJmH4Y+&J$vtxky<#%<^D>^{+G7>3^dCD;;+9mOqaLv^~l%0r1X!IPQZl|{$&LbYIwFRCzPosH@l=6ck3#e|@t-B0-1QUA3PO_hu^eko zi92V3{p(MZZxR@ue^v(;ece?qY&D|K8l99Q;Q$#=gHg1xcWdiHN^?>}8-g{xDpI(_<> zw>HWX7UUnNdKhm0JBFT@&UR%wrM;UkXz$M(W}N%WJdF#=FLoi%Yx_OXk+`Z|f&K$*dXY^#Z;~vCR&r44Nke{D z7FK-k{kZ>uVRRpz>i#bY`hGvTUD7n(momR{x8tZS9)Z^6kAH@5Z>_2Sl57z;3Oedg zkof$Oj&aOcDX(c2SIeDTyY_4r;dZp(456O~cvt?5=Y{l_FHjq}kB%Cy(RWJ5o6ufr z>ldTmiAF7iy>PMo*+gnsWTnUwA)FSdP9!i)Id{`vU!(q=B_sWIJo~9)8*!0**RR_V zP=17iiwl51NUb;lmNkmNc6B_rgSrwS1b~UF&KRe@0W9f4W{Zwjxrk1wcDR{5-GfV( z_tl89ni|lFC@{nVr!h{M8Q>X9GeB(OAtUi`^er6C%mTgJ#{KYo$A~p;8gSPun%z%9U;nw>Ou;ABZF6mM;#Rq82lpPUXN+ZixS{*f)cU@R59Wf*HF2+N z3^E-##KK*aZkYpCz(4Kk_ut|)&+9$w1mh#C%m9RE)jzC~eYD)xS1xbE4XA7ySI&3@ zJDz1x+E1xB+Y#5%^l~#wgMyyVB)e zkAWQ%9c(jK1@_U_<3ESdL;Q~E+GZtl{=Vejivg?Kr%JFo!>sLV%1Eq# z$2+y1rEM7H81v>Uf+$|&c~h)oNis?fzvZb#p$$~k-!*48w9F-1kII2Qbr|=2+vLfo zzh55c`2jC+FY{~*eE?@o8`~`jld6u-0YkHd&!XUupg-FhMTV;rp8w2RGxA~@lg(kR zi4AnOxU22AZ}PaM#{2lWfrtWtAbf|pQTHy1L7IXP1OS@b&v?%u4?notx$h-Y;Fl|4@U)$se0AW?(X6{%3*U)*R7)pW}$U)?l&H-{a ztr2|6LVAh!P&YcH#GB-y9BulmHkGIel^60;AVYG8Wr3Y2R0)I zmn5Q1ABeMaecI;@%Yt!bKd_l0jw0XJv=EZ1tGshwl2C}w}!>v*%q||Dv-^Z?GOsz@tyewVq!qHd?Uz6A;Wd*{vxB|pYZrKd&;?iH0Jk4qr#cL-V`9d zXBaG_t_tk70J3AwucURS#sIhw<>WU<+>J8tL)Oz-#+?wH1C3i}?d&+u1{1+yEc-lm zz4T=R3?V0lP|>5li5)&4I4z)j;Nj;FUpdbYC`Amp={y6at_5_qghbSj=1bKW{DmI2 z+3)uCJ=-b^{|~)%9Z8s?FIVbMgmDSvWFm?&jaqrPu55q&lm9iLKi~z&dzSR`iN5SZ zoPPie1^}3Y9rY+%Dn$v@p0$=93ZuOzcM{rDaLxR**Vt;@daPJ<4RDzx1D)p%7RgyhDy`;`s(b_A@s+9Tb z+aP&gPB(;n=8yHeAodBEH~hpP2Hx@ay;THj^TA~O>Q`FOq`Jc#b3QI(iWktLbn5#eVhhmx|swZGwrp6Ju#hml=&2 z>y25~q)SA|ze|t(r$GGOvO3m!_4;>3j#X;pfdy^90RQwi;dhGpqwlNJkGZN#lS^dF zq><2wV+PThU>?z!3-6X24-O6p;Fb(T3Lvl1@)bSt|6b#F*?rN7xg|N;zYQ=dwmBr! z2)T6b17-`4IJf?xKgK9x^wO0zs8H>Q)z&^C7H}(l0vk3oE`y3|z*ldnSn<%G^^!@w zTxs~aFkW>Lsd3RkV=4Rd#v)tHMs<6Az)uW6udX^XDmZTq`|J~sqI?r=`IN79=Wij8 zYJQX@)L#WkjkB@nH3$tY!|iHs4#gg+x#xFjlr;2DsY+#sCoheCa0oFaTlvS-B%6k$ z?&d#_S@>_5%IXN6v!D$f1R1UgHDPZKA6@tB(|+2>P_=4uea3>O+a|F0sZT0d*qHL8 zs}l-%FU76<&;rR6e^<~|s~eq+q~z;y+^RkN;7Lvd_c`~5}L%l9u6W%!JHNt~t% zU(ENR(@K8Y?;HdSwLe*kZzq$9ez{|Y0J0xNM(iQ=F>}sEBQqtV<;?_D=1e*Yrg-;7 zE?RI~7S8QV$3qHxUQesHyEws*z84uf1m_q-pVy+$Hl2!FtOD4RO*TfvvNg&Q&Vxa{ z_T5x{FF?}x3MF-vbHfB4&jgbi0RfmfjZtF?x<5;7w}$p;T=xQ~(~(8{efD8Z-Ju?c zlvCL>=|($UaX}@?>v012VbUkB{RuiSHUq`0*Pr|p>tfYUeX~Q&*;3!&Db|y`Dg2Yx zTebcN>8N<f4w+hM{Z73Y2nURHEoXv1Oj?#fH2xA)2v@rh%ukq17Xf(iaqE}q-Xk03p};DOw& zNifJQ`%yHeZf__ZnXnyTEK}cvT=fM-7($JDCHqy2a35H1fym_ya73&q(GHZ%cM) zrpw#7ALx4dY2xX0{s;%!ix%+n5)}{3DeH4HTzhz2+~2XCj&sr+d~`IU#%zV`(K`6~ zXVbMm#cA-cXv7^D`At+FaS0cFE==TZOQe5ZAIA}*E4vu02{t6B>#ycO|6Z7qm5QWa zcnYHr&N+CD4g$R+DRas;bud`e@rxJGq93P8y88S5kIuCmX@-HRi;upexrnFT(LP&U zK^mSHMtSt-?U|s|>i}+2WWxsq=127sMK0V$Ls~++Bqq9vI~DexcxH@ljB-gdvVevM zBl8z$!Id;O1a9x=g1;Zdbp_?~n@?pYFB>E%T-wDJezafe?#eUZikrAWF7rb~9|b#L z;4@8GQ-ku2zQc@s!aHuyT;`((@#x>1qbZneU<66|A<*~aA4H^D>37Z#yt+Y+(aGF|xLlL42L(*zVzU}eJTqa*Orry1o3oxjR{6PxWNn>FI(R=va z-&39sT#J8riN1Mi2F;!Q$Dva^I}<7Au6iI`{3*DYR_4Rf?_EVp!LKuN{=^QghgAc6 zbVDWxYb4MI@isXZ1~YDpucRW)^^078Nl{1}P~+QzhU_Eyz}cEFHCgQBrB_1}I8aVf zFX1%C(j;Q# z3XTghx|PjrxL>fHje{(<_M+TFvra|&G9D#PQ8Yo@Us~<#PZbGqsdFm+&pS9B_cm$L zSoxTq^k^_Y$&odmJr3&yOUHfcAU>!V$+l`_ny%cBNfr7(cHeN30&BnNGN&1mc}vMZ zImi485+(meVp^KbpBR0pTsT9Ia+t^vwa?_qD^h{UFuXWo z*h=eIsa4qB{K($TGW5#9CeM2`fr?%+pn-^SvV;aS*F4~GI)1ibvTEfI7OUQT2a?5- z)XTEGIzomS(xT@yAo$|7qnjww=f+5Y3_AP9^so4qX2i1VdM%O?Z#xh&Z?BjZI-TNh zf9KVIV6&Eqaprg*S-8vYPoRM8PoKJ9&6bav*!kZmSyxZNxOT$b` zsk0O;tAdcAwR#RaB~yI?PLM37A@t9tqW}F64XN&e`I@a*cgM01iybzCR>ADj2%$*A8ccZJZY_0)oxyp>g2lp#`aExRGB zf&i$-%@mY@`sw#xq!@y!-^UdJu(!iil^IvD);Wc>*{HcUYngTACs=h)ec+*wyuwcN zn|b+rv>?px=z|S!=_bgFoKLxM2)9IZ_XNtB*9dZ~k}2%BET* z4*N=2-Rz|&0b3;PK+Ln_++5Kw8KwQJuH?iRs0RkeBlS*y_gNh6T11GLjtXwScxQFo zU%x!eYS?=|vDq)Mno?P_X%cp6EVi%1O|)@~PRC2W!-GMF)$Ap5^h!4#B>nMOrRb#2 z&c(A2LOK2YkC4)Lc&eo`PjUeUFiKWvB~&~MMl?t-kc|~yiK=LKoI@{sN)^%aOur_- z>^1k=+*#|ZVMHMAL-Dqamj)|64t4f1!M0YAv&w~ITh-UI8T)14Y2e-t;uHK!YG%*) zSSc@t7VIT+9pbAltME5RwXJ%_6-nGaAS*lyg}MkjK`H!WCdbXvjxeTR4VpZ0aZ#w48ZS%An;e&qZ}ILvu8IiAQaE}y~#+|X#)dz$)1Q({YUw1 zJRBoa$8Q+fX(cU$}UI2 z#c`AHDhp&tK#$UKfXu@?XQk39iJg%DFKxJ&CSG$B_^myj2A~$t(~?Bn6sDN54$&Y6)(%O_9~@0xg2@Q z9V~SkZZFrCH{(;$hK)(qu)B@FotG(6sFx}EdwC?qo{H;!c!<@U7UKDZFNq=s9px_9 zEL~76)FgKHGGs|#><^ARxZnEuQa!Zox$Ofls6L({aoiEyn|u1eVgK$Kb^NQh4p4Yn ziB2&pK!Z+fik^hX9>oq9{3%h+w%K+%b>zGdxMyLVALF0yDz6E(+ z5UouPEuZ+|B8xo#nP?hPg#)<8n&K>MieJEp$2}~vL~bc?Vc^(EsgsT7g?5{f7e4;* zkFw#RbT)VDt8NeoG!U2?8F2EG$pX8;o70Zn(D>0hPMRSfgKse&v|kirORcAXjOWyu zlL)!WM@H5rKVGC(c2TZ=V`Ww*H(uQL|+9fZwV-IKxNP2ZHT(UnDl-JonmPOj4ud1E=ifKRpdvj6P@tgr{%^L2U zEu%7>Uyp8+p`vOM4hn*#^Hx8W4X=c{J)e%_^x3>*du=cJK@BEs-qUmCN<7#s_|FiD zJ$-GayysWM4`nR}I^D&A6@FaQ_n%7V0C75|(7*%yW5Uq}B}*aw`aUaWx3#BM8!6l8 zN~73OJ5*urfV1ypIG}tER-Jdc(g4Yj&(fUTP9lxWhnbfj`nw+S`kWyJx!8IJ;! zmCM`5BZ}vfhwO796Unb$E)gHg>amq^Z{zuXVY6z#@vW-tb~u%=nSZT`=Iz}t%r824 zt1eZnOqN75QKkYj^3!!PXFY&MwA|znW8^bnhwga+>&9h6Jh<*_pZTP1mc9`G}72BGXfW) zOudXhFInXEr{hNRZDKfx$6wW90z3TZ8jgBm48_nn>$m)%=-u58<%rYR&_e6l;9 zoYC{Y%b}pYcLKTJM*Zz?SyFkPQ2;TaCao;$%H+Ow1gE!`Q#T~(+|H4ioPVw`{Do$6 zf+;!U=FQwT$W&(1pbtnuxKGb&(q~4=*b_!}9RG%1QqlFaGM@#B+KS1ocC;w#4)4tI z2XE94+L5iSyu;KVP;|?Y)D-gqElE_+iQ)7Gs%foEH4yvx#6n*u9x)_4>n{gUP48Xv zilHLq2@371nj_;XxzRbZ2}8{{F!i|5N6SZev>%;@duGT33P04NY15rngxd0vO$UYB zM>Hddsb0S}-ZGC4ag8&TSS>iVX=DBwks~;ldzwyeCN^57{dr43K#MAV318P_`E6NI z%-Gp!DH+A-&{lJC*sg=nCMcnfwl)Pcf(!BX4VZJq3t_E`8DF>Ld%Viy);C@gcU&u; zh@I6fX#cUlF^vw$t+OxoOq4H8!RaTNCyvzb$mi%#Bln+s9=rP>^*>2y6MsHloC;jp zQh_i>n4u!dZ-hd&3gES6Mh!ablirh(cJQS}!L<7h;|e0wINvqlY3IR0Q>S##|LzMv z4*gTkD`H4zsrYgagCCF;`^fe+JlM9nHqaBYN~oS{S2F=kn}ng*?E0 zD1lVCcsUokXlZ{vppQSWO(gjPoy23DY49qi-hHfjigyhTfBY?)N7s2oHhwmq`YPj- zXvR@RBZ#5vRABG z>RZG5*=nuDgH|WEq%Gv}6VP0b9*MG!;2Qi~9FHZDS$p4>3)vM5IV!A#_G(*p)!~KS zax90vBHwt#j)n*YSAfnyR0@am$Hopqpzq$Eb}XOR-1@g(tc*>JSxKFrKlF_e-}K9( zQ~OF$t2JkU!X*zFX04|13bg zu`!0i)Mbay6IR2nZpsS!2)q*2EDN;gaNj_=3sDkydLrF8-%aR}_X=z*hEdEq8c@PMO`}+E9SXg^)+oFwu^01`qU4`C2Z}-~82Pi39_rYswap&$ol~U3eZ+BT`r=wY-2IAYeAnvb zkNi2Lz<UUK;FQ*L{Yp&@-LT45))LF7D_z-Z z6KNSmoJUOD)=)Jxwp8}^yAxDQtp)lT#v`7ySzhmW(=0e09PFF&Im{f@Xa9lezaFUu z5+eaWjP^XLgYC&C%^5--XeDa?`t`Y%k~BM^^dByYLrkysgrHQnp+i+xB4Yt>Bs#BT zLn_+8XgKbe%`QQgD5SQp6~`deNP)=VwxKg1Mw@}n*VwveWL2y4ILgC_|!iL@vzDI(<_4OPo>n&dE}YFc8m8g$?z zW7`VxR92}5fIs87!udL?`_&{HN1|7oQWjU4bibE}%kK$#ym>sXvG%`o1XMb7RaIKu z@YnTollRfyv}O1T6qYYW3$%X0uP&g;EMDN+^uY0M$&o&@{zytYvQ<8>+_LT6(>k1# z@uQ@M{YDG@hrh_0VO+@uWnuB-W`D5zA4=%gubo<6AggjBoqxJ~wH4O7T4$EWG>8sl}?m#PW8)+W+}{;c@esIs!^% zm&{JzX^&cp{-M|}f(PdPEemgdcu!;q85f7Ip7Ju&iG z%SNY93m-IHod~OpqRMVQE@L1eDJ~T6nTYWZvk1X1rhG${1xdJg-$*YC3JQe!r*KP0 zVE+I~zdMs(f?PIQOhKpm?p0H>eZ3+O1#qz8fGV+TSyq{5+4)h0<=v4O{Lb!_K*=_J zRzhl2^T)Te=jep5`||inau8jzhYKM(zacf)C~r+&*ODp$1*&j; zB=?4AQACrOnx z4R)pXe>7huk7(>4TGY&n)^E)c#wg2x24fob3|Z%7v+#0pJPGO`!f2pek{kkj;Wp+b z@o&7dNWe%3qcm=r=++Sxnaz&rVZl&{w`D)W{!p^$-_+Of=+0nVrdfzqrcYL(ob(s5 zC3Uf#&o8bUFQ+PKJmkfYYgaCl3;Mktrm!H_2Yy%wpB6i0(_iMKJ@=jRl8`Tog6MI& zk4{(}4@Q|_5RsA4E+lVJ=PKCvHZ#!la$D-&-s!?`p-{Y36Qe@CG69(Z4OG2T#;Hk9 zzidP1vDtO}ifi}tr;&K&D-YPnAbFiGLUnVzc;L#PCvnOgWs?Jona?lyP$*+oaAi!l zOvj1(yDS2AM5ey1TpSJf`=4(pSC|&P!&m6KGU&A7J&q^SzU^THebXEcJg5uE#oZsn z9*2MaOX~O0O+k$Mi1h_0$fxe6zCz*)cr?GPD8C<<>Fxrm=svCbS$VW-?rF0;!bL7+ zIDJ@d!k!R!>YBO3wHEw2XwCb8x?xx6-;zm~7B6Zi|?EetZFTP9?N51u5a zp?w||SzA)i7v{}haTi_~A!*p4O|pY`%*xG01|=@dwX1aE!4!%SA% zXY@ia#v^ae`}@xQCJGuJjy|wI&XK|I4$4|a%gU-WIE3N}U+q+xnN(EWsyRqb{E2v#t(z zZMt5RBO~`Gqy(^nE0snpQnSghy6cJ>1x#br-i={<`PR`EAo<7EK|H&HLDvOeGkD&u zkEU%KW}7x@@%-aVF%~n{4TvE`Mo8nxlZ`4$X^BW+@|2joB%YdxOdf=`X~&Jh_ENu;QQxpkKdiBmq~htmkXpADRv{(U{KJ)c zSVw+JJP5L!Pxu8ZbFU<{K|+9!WF7$ zqS5rbpIlO|7^nP!f~^?TK-1;G2OX#zsIADJfh7viOnzFB5qN66WH!sCgN<9=jVqDe z!8@K4Y$TLE2r8{V0N;m%{5xM7S#}}7JYMmSf(R}jre|$?*GZ~~zsmzIoQ^XDHsYlC z#}N5N^t{k*OG{j}!(~hIgo}l9K$P9!b;Ae=sm{XP`ZMG6>Cp#Zuv}GNxAKGg8>p|c z?&1E}N%3Pi8EU>=`qV1zs-)OB%t)jsw5z<=*1-B0100OSH@lOtwd$p5R2J1>{-g;d zwqQG^kQ}k;d%?dz6J+e_Aw7q^;N8Syx}jI8f$P;`9@_dWxA43{3_5~1?tB7bzKV-J3*D8*vpsSpg#z8Zqt;&<_e6(2kig^Mf2Lli3I@0~?-Z_V1 zlgXb;#T_&tQ)G&=-@SR}Czwyd5djDOiYMA5hAGtR7lzz;IwM!x*r*H{q~t2+M3Dy& zwgKjy#eAdWW>`%laDtk={M{bd3&=>%eDOvUHwxaTXr{$9p(y}Ql{5d9t>;ZFGV})# z{>eyUo(913rp^ucpt`k9Zp`7^)0CJv^E|nL^JAf5^3XF{rBEm7mc)7cH>C|K7z;LU)^&0;F#(x1kgpndMEMmuB0dlG)R2*;?{Qea zh*6Q`Jo6m_&PVkE=A&5Rb0-^U@q?!9d|;@`P*3Z&9o;WiEasLnb2l^v4u*k@anGeH zET+B}OxB8T-f3QyArQt2%6i>T0@ z##})nh2Zt1zhH+d{pH=R3&MY&u<$<}x|LivS0n$Cc6L;`rzchj$3X_7>XPNdZpZ(y zwL`q(PLEG*{Jo_Po(f^Chj+7$OdKtvWG-kTEY=kU_< zp#5>vi&}IFgo`uhXnrp?j)+x7(!6jqs;*=kYA)p-&?kyxRdg&J*nGS%09-_T zpO%EXz|GOP-M?K*q)bR2OJKqVvuwBmNQUIcdu2nC{!fmPr%}}ULXr3}&PE(c>Ls(c z7aPdgQYpK;J^J%Pg59$%Nui0(vXLhJC{!}HY;%5!E2WiM#qaTOHEv|so1zAok=p6{ z30?Ot46`ZsJSvpW$M2l@es*+_kbhc}VUL-_QZCww50EQ8+Byumy$$-|+DS_T#K&c3 z(tt!ia8>Cm(Y=te>qmVb82AQr(Yu`k$!4~L(=?Cx!Wp_`gn3##{UXk%kQ9WXNDuyA zgKW=@)+%DHq0qCF%Oev*CqRfv^D4aGnXarhB;Taw37iq!raUOHw?>Rfev*z{Yk&qm z_&Qy^xwK?sf>>A~|BePKkeopZFvvF6rA`SSZ*#k&7D{%gY_99Mt0JH`wf2jz*xkMJ z^-eoT_4=8J)ovkUZYjO&D#sY!|I$&WbKCo(~7hIpe9~jq3iDt!>>eE|7A!Bo}=pO-?RyYNNNSjQ#Zfm8c4wX8&+|h282FBH5@^Y_R*h$&kI~ z*LlB0X|aZ_^-c&C^6e-YgJ`MW`6a8P;c}&!cT9UU_}{Oq>x-MF7ygbr9NaJAZ)wdT z;t?B!CkJSv*-PtZP$@$&li!@uW|&$Z?YnRvBOz@~&|wyN zIo7XzX=GG}J(y}&H*yYTU_lr&kpobxF|E*+^>Oycm#)Zwn}%sAwI#0joA(E3L2;uq z*{8kmr-Hji5}NnWup8w&j57Y_@!rs=GQ784h zX1>k56G)>&6(gjiQzGg(%t==2w9#AA83{MjD|Bq>Tw%-47UMm=^c(th3gFm7NTKb% zgLzo3^Fo+D6hG{{mD4mAeje;UlxRyk1hf6tuh^r%l-PeySh7t*6OpG1PCO2zD=mxL~9VEEW2$w(Etlh6y9w9zGg&&>|>Sp)8 zzjkg;m0sg{` z=h{?#(N^xq<Je?d2Gb!F!F%novXQfr!vZaLz8fH zOwX_aOmsq})#@w`uvM5)js+2pqjkXoLDSjci^hy9EZP>zr1T&=G zn@MGx7n;{MS;N7mMr#&#k6V8TFsB!io^hmFy~84U+VsMTJibjR zS3D0SO5h9%L_)4J%CA}4%%8kw!7~E-B-CjfB<(PO%iKSq%V%=ARU3S$Il{&C^y^-N z;R@&SA_b}6emJUcBn2Rh2C}ay5|;u{6Q%!~XS`rk+&6!tgJM6fw3>l6b#cubfk%rz zE3P<9`rY2U?y$ysPVrBTPl{!!T|gPR*|HZA=8r*CjyQ&gc+@>JkhB_Jf>9xvSL@9r6JBQSMonJorPai-`BQ>VQ56UOHvx? z?r!M@1CZ|SMoB?hLg^0a4gqP97`nTWj+uGS_xFCDzu=sG&W^S2z1Ds4PSbwMK{YH= z`sJM3s%i6H*$6aeul9t7*TZC|SF^U}b(Z(7aA0=Guhk1lDmmnff0x6^uD69m|MiGS z(W&G7Jh|5fffWkwJC~p5Uq?xVMBj{@cX(v3iwOR)^p@~a$?KwFX1}rDUSV;)X~dtm z%#E^PwIMh0TUJ{y!H~}kPIAh z-2dw3E%I>ZZpgr?LQ^*ZyP`(Kj`LI*)F}g0%Sl_D%sOj5Len)mBuH$ipP5afhL+q1pAW-VZ=8^U_Wgt9o#gHQ%cqM`K*UB z1*^wPi_RpV->sy^jaF{?*)$g7{Y$qvd43Yh*~c3#%xDmZgb#Jjs<^3y^m2U{>nyr!Tkb3JJ%YApwnqNnQy|ubDQhP{v5u0_jy54V;%P63?>lxtJ>XRi+yN`=+ z=0}O_LN|Vy2$r2Vg!_Y~r09jGdoIvxa&vZ`3?^M13)35pPqMSf->!)A-4t=s&Rtq% zHPe(-9eZZ zpsJJ{DbH17Q8AxWXmxy()uZrtQqI+C+nOY!T4&NXSFzLF#>eCP`>9^9n?r@#U9;XT z{hs(}Omx!VHd?6|m#=x11pk&AUhJa2{dBxg4kt+u<^a)q}aP{*zQ_S(eEbmh+I1yz0pwN!$fn7zg>qd zwolwltNO;?a>`i-?I0;M1-ouW4ARCI#S(gXf@frf*wEe{dFP!<;t>At!?U0@!^*ShN`t+3w~5N~yXBY-if ze3dma(>x>ui3wPl0oO)Jsz%=r39!BUB9UUbs8M!wPTA~{!i?>ba?f5-kFWpyYuL&$ z-0hq-0>!zCo5(!+JTZ{@TRlv~NfHCpz!tZC6w}UBE#bLdSk?5ThL;JRQM;QpV`M@! z`I&YbFzx>9hgMJN9c*x!_Kvr6QdF=(=E#L^FPX3~8Tg6|y}Z161qAw4 z;9~m5l^1#Tc$QXf@(O^!@BE``fpOKD`#mmot7uX{l*o#2#i7owO_JFEOcrr`s~ufM z))8+G_)s_ADC1X?XkBi^`sRKAp!26E^__GhCr}0FNIzRuU$`w`_Wy zH2h8Cn%g)|f=if%i2JvqXLDPLIg`e*$S3hf*O|asH`m?|n)o0Fahny-X@_`Pbf2p& zqeoH9I6$zRUX5PK&a|>|4>zttm8SQswaUaR2~zEh`4*uZ2*%x5z*$V-92`<;<#{tE zSc2NZClX=_&H8}~9k}_5NvJ-#j$^6Pra?98x-E^+pFLNkL3?bT`?3WBBHQ98nT15a zD;VDUF05Mh+ZX1^To~U6aJeVxDWNGDF%T=H%b3SHR1UcHJ(e>9M=GFa34xQP21Bdd zGPHRyF*`);>dY=#6p&asjxLdh6Zoa7?E!20uzlZB-$y;vzfR=q+xhxqZ#Bz*!WHAV zIp2IKgEP?Lea-sNO;z>F3^Y}J0hQJ>m82pB9k|kdfQdi%pJ_0OY6q;CY*Y;JQ%e}< zcl-iy4D=UbQUZ_;^xhELN(fJzQ42>;J%{!p_MvI+W~c;bLZX{zVLDqur-#kVAKHcS zg?9&?O}1ZGf6^|BBN*VNC`!h>f9$zW3{OVec98*&0Ee?MFsdd-74gr{vebjg(hLSGd<1UOZ5}2V9E+ z&CwULqXCWX^;Aomm=HNUge{smMKc8+Jj^5!tRMT%x=&{laqoc%O2}}lw)CQ>O;0nW zu0LSwxxvUzEbiY8H;Xo0U=7X54_})=B0ft4_#1?nk>b{f1Cnw4DY6P>T7`WNCbnHC znS8H$SZ2cE9j|R~Tdfip=}40#!xb#1k&%77o#Cndr0$vVC`*EE_oVJipvH@R?SEKD zz*bYdHO;8cw~;e^>$14NkXP zebhvuP+m_sY%RoI4OleI%`FpLV95)pn};C%Ix&+vD3uC9pLsN<+sO%x^HyEJD%;WbONjOgj?vK2X&YZ~ee6;rF9hf6+Ik7L4a zH)U~TJEJusc1P3z_D%i){EJI$b|;h_?dZur`wyF2$xvqYC{dY#Pu6<>cEcEUdww-2 zHGS4dUJ)ge4yj5I|FDCO&fdaLljP~3(DtqasLE<;X(zNSk@2wi8$11yuYCX*lh$^2 z<{YfYHJ~jw5o9|4_Mx++Em}!oGmb@FGL!IQ!1b|L6@OD1VmQ@o{|3MNZ*pCyeA+Edmk?;L@F0io)THbJ#> z#_AWR=pIzB2i|V|8ax^5r0-O#@ISs`%rR)*wr)JIDqyf!US4)^a{71lkj^41>k~w} z;PqE|susd0)pML8(E11ef*XQ_W+tVC8&#ld4nYmR6}X7afpFoxh~>&;}4?O(>20MnWc`d+k#GU z70>}BqS1Q5l;XZ?@;=huK|fdA>$|4l3erni+$8l#gdjEb3Ki}j?)IZaq}X%?NWzN> z(Ram?=)On1T+f=M;{Nn!WUpBIL1}JO5}B92$Cc`esMQIW&plh4c2b;<8SyBDRwOw& z1?&?6@&LXT$P3E&OR?pNzLeX(eep5YdEI<4+e6K2!qwW*Ea|uoiAgm|qC)xwU+fs- zv4JI!HFeF))5PZY+rXn=8KmCrAX*HM20NXdqIn z!Fa5m&qr3Z9)cC`3cYn>ulhciw7BADr z8>tWmLU>9iv4NuOX15-Oug&;Ng6bShL_rCVmc%h&pq=TXNMzxZpzY0EjAZoOpJO=HD4YY5&7-?KtPn~@!tdPH zU46)8R^i-Cm*T>cI^DcH-DNUIbb35lh6a>a&c@j1X&m{?oAFNj2*$Hfk!FE@e)hnn zOoevGA9Pq50bV%*0s zUR*6RN+m09k(5{^q6_7$JU?}|?gnLl?eh|Q9Y5c|uhcoxN0ZiIEI%0Ql|Z8?4nx`g zoP=6dBks-H?;zD9(Z)ozUC%uK72e-ScsU;G_q0Gk!BewI$ju+W3L96fBgvdT^*3F<(_k$mfr^ zwCDzR6bHDb6wQ5wc8y5})g=98bJ3Lb%<2DMsuO!PQf%pK^6#*6r%^dJj=ojE>GJqV zBHLIyHlC;Q+SKx>Pmh?mSlE3}Z3djLf#AhZU5MA!r%&5XD~_LFZpHffkLlDHWKu^_t*6zGfwqjLTkN8b zt%XgBR_c=r8Wc-NSf?GlFqi`Dy;0h9d4AI>gxmAg>X)B!-%pclJIdCba3O&6`P9tv zL@O`|kedYs8W%GtfdUHo5#u+Y0LL(!!vJWKJe?WBhXE*ye^CPFyPNmuyqk z62k5JmG8tpWqP4l{avt~i_)i+B6>cZwJDdxFf9F;wY<)GnD#2gzv6#h>^={tG+Wme zMdo5t92$I$$Y4Mx|2Hn+WBH1pTJ&b%`((g}YDC$+WYB*@NrL=Pw)fLZc4Od5#PoB& z*!wmfJtMJ84$!UP$!-;!AuJ}4M}^D~E*&mM0rRr4IQ*ytz=5@m;>c~+U7)8}|_ zG^I9CK%4aYxDKDCdqOxN1%cv0ay15MT(00wGnax%pEv3@bITdm)M_vq8zOs>PY0I* zek-w7Mh*@RmdgVB7Ecnt1}1Fgj%ZW|<3dy3)#n#gfmG4|I{kU_bN{jBZu6F>lXaME z>!-${4{+xp_70y88~fRhh1SaCI}PFW?&i31wnfnCObS7V#JiOGcjM>#Cym~jtq=;i|?VdQdy`O40=oEsSq;khl>W96;YS5=uA9Z zJNi{-b$uoBdiv=Pq0SHF&qD7AplSLud?q>F=+=ZUdX7j3%M^}%&Sc0*tZK$V(+eUYxcZY ztcA3^BE8=T{rHRWUq@Gf5bS&{d1p<#rQsaOXn{o>H3t<*0;L|M#Vg}AI{U;+pziMC zmMr!gN8e#M|NUX*_)XV2{sq=N6gs@?rY>1%hcbndLk(Ee;R{W{MvqQH8NMKcnZ_mU zYB;&|epF*Ewmbsp0wGX-Gku%)N_X{m_6n|@MHVQ;i9_kgo8U0$_#or3EgYQ}wpRQG z54tBLT@xlBICdywRHQ$TP32L#ZGzG{?VDoHhNE;S_4}H=*e1cg*>)w=S1oGvjs!p3rk(!Sy);7Hf+`n{M)DhH>OM}CgI z9Me73Hj7Wf%uvxd&YW{eAT%*%>pikAQgw~ZRCz}ijg8(^WYG6)+M|Mlb;3|*zv_b) z&Xa9+(B}`Yiq*b*1tqmDzanUvI=97-s;wX!3RNS*J{VQ6*jEQhYuZ#bmr;$F) zWo$92$(RNT1d3L7V1Rx<+}^-rmz|2JfZN@MV}0We^EJrD^|4wVs4ZWxakQbz1n*}D z@zpO zWiPS2bCrM|v|unCOy*0vw+9y7Dz7w(FwQNrhNw%@*feQ^T_4-$0{30d%~un7AF@Zv ztULtu7*6On*EQ5)*Cq<>ao%&h5wBpu`3+f8ljL{$5Ez*AzV6RMGXn3|TvyZbb!9TG zhYc_*Ka3PJlT=@m{NdI)qDT?Qo9OGCpXOi;P&u8k#qQDh`Cgr8_T8oN2jTJe{A0um z+089HbO+85)FAHDt7#b$XU(p@m8v{|69l-4`9~N7udr8^bvhTb(*mlD+2SKhRSQ!l zX)CYQ#Gh(IkrKDYzwiru6rUqShP7~RsOQUdH;;7O*d!1%F|ny6gGBh*WjSjxxGQ1utd-_5A+@W2EnbYdh zx zxp>aL;}FtZ|Kr=FPyOFOy}j=wLP_6rAFSWO0?BpQNJ7d{%7^qq3(aRRK^YCNU?do} zo}yozV~~+u?`F;&!IXB{vj~Qee1u|PQFb1^G$pf|#}8aIYdXPb%rA5=MDT;stTx-6 z+$HP5WiossC0!;k*ijmm&ax+We)joI;7tam$gH~vX^g4MBN84j@O z|1-m@ftEp!g%W;oS*qMkA1!@mCH?7E#n%UP9V*N6WUj00V^M+RhUkA7)IjfAM1R)1 z-o+w=m`*t~dXATx&z}GW9kBW5?gK>H?9=KwyKyf7zG@wu!H(9(t+Sr8)?6M%@%8cf z$al2fg%XX$%DuX6TS#3B7pAz)kUjo)byb};b?L6d?#W}Owc7RAShQC2J{u&x;wg=L zB3zxD2!<5!uuf^)(97*6zsA5l2&@j0tl|rXQDB9I_;un)mFd0*7P@!WlWbZ}y3wNT z;+Rb`1JH1-(C%0rZ}QPpc{RF*n9)03X7hZ3)=H*6UkCT%_rVS*WdFfzH0t79+=88?RC{0 zzFDfMMjF8QxC*Jk4ANVu*qw7L?=e7M5Umh0L0M$ zqLFUhCj&H%glz!0lm*LDDaz_&&l%oqv5m0$I^p?s*fUCINj+u4%W@GX&2hUJ%TNRJ z75m>-9Cl{)PVGYd#UJJzn+D#cdaAP(R?mK!cD5-hw$XM)YK_`M4U48lcQI?mpqSlUg6XT2ou|Mfj2 z1NZj6rgBfoC}p&?Tk(_9>!gPeG3YQlc=j$Yg*GGRi{7wSI^FZ}i_LDbBuzmDz{xS0 z`<22(BpMqMza(D~hrWoA`!3a9*JF}mA~isbm?Mm+l^l{dnb=RvD5IwRL`TG4npNia zGG}qX7G8{qc~iazKWI~Ok>8!IFSn9{8LXnP&ge-inzk406VhS-WldE9tz5x-HM7> zHKrK|CeYvv1q?Iz5mZBOz{o_DK*eyL+_u)_+aGO3vu}zuFx1>6f5VoFyrgPG^wpNz z2AIibrU_U7+#!|(gL^tWbLJdv_dF+GBM3sExbS^c_UZ%-N)xRzRf%kjjOC;q)7tmR z&wME;;t|jiKHU6&fA%9aru-#^i9`0kW%UKKszn-AM<1Iu^VV`8wepEAps?l+lG~|UjYC}0TuM*vd+Wgha^?Q36ex&Sp!XhWF~@mx+)?;LEkOe2UEtx>sg2)hP$$% z%zKkp^cstsKiLw9tn*K@Uc27B>>3x9?C7nsKy(LOUe*9Kx*~vdKRrE7*p{jisZ#XU zgGP=Rmk3=B@s>U8$V+P*-viM>U6S8kf+LGY1V7cLoSN$`wWEp&DgBl_Fdz@cFj$At z2eo}0#L9hxktZd?H|PiAdSPNpN-U)R=T5?xw6=pPNwv{@fjLi~2UUUAa%6At+8qGb6T_4$w6dP(^_A6Xc8-7*1b8c<6Ct?j-`)4+g)q^4{5 zVHE0fI8IZa()l}}EcN(<+<qdtI}oG<~UE0kt z3YOFjLhcI2LE{HVv)9+U~7N)Lj!wkw43q#sezx z^)Ur-SA*f|*8qv^f+RSS0b&zi)oycTz%L{(r=Nm{PsaXZ1@qcWOhKW|%(Yr7t%?{>^B@~dFWtn!u`vp3Q~Xnq0@K4n3nfgh9+WtOyUHXal8>;SK1ab;ykRPwpTko_6w zxFT`xJAtfr)Xeig+ZG8cUM+MplSBcAyfMgZN0I^6?F#{9WSUQJkY>1~EH?;>Q}7_4 z9>oh9m^UUP-S~w!&S@DikdY;G>PTDeKUA9|lKh(`tt>4=bXnyajaLai7NO}M!u0|} ziDy=9b*QFY^_Hh2tL>1uFz4y0764YI2OxyK22z2)Zz_tt2uQhGL29l@ZDf+=zo?}0HRfV(Z|;exT;w*iR-wn* z`~jp}7=dU^=1*EBgmib@K3-JH)fDGZx*GTq-?C{O4jy;~U-n`ZmuuOcx3?g=dEI_$f)B@*QfOd5GAf1xkW@U97D^W;3%tfZJfJqngq*Lott!Y>swlP-pD9((K+gy36K@Hh)HR_i))5o z_;yeosg^8WamW=g(kswqz1({VH|hFUt#d=Y9r+!EAJJPy#1(hl&11JIf=?FV z{_HG?>EfWYsK`77G-#Fx$GJfxUcwmV{4wcmS1t zBdNuVwD{L}W!N0`5V<)08)$i<-CI^<{lv_O?%ZC@yz&zJ`{`Lp{tGTJXj+p9{&v-l zg6>zvaT}ml@9pmcKy4ZH-sSaY9B0J6h4xucG}pYpkI%WsLbGfIWA>c(OJ32{fTtV8 zXUUd}_5gS~0!uImMxC9V{ojY+;R&-c@_&?Mx$DNn1X3m&vJeatnR$wT#OCwITf93%GVBuRVNsIr@$5RI#}x9r{Qk* z3K)IJMMb6N%5m$X&u3cZcwwFWxc97xk%-_I?Yv46{JD27S2=@VTut#?iM%n6}QUKs*rxqvmgTkBW_bbX_0 z{b_$^5+1_U%-jOIsq6T%pIe^0QQ~ciirGEj4L_9UG=Cykd%6l9KV5DHu(@g)8p-3< zcj&#ZkMpSsEZvX}_B{;3?9`S19ans4)Jw8oX{?pJse#ud43u_Ys-3TQzwD`yyeA2$ zUm5z`bl5B{n(9J+XrDqXYX-)3NM+2Ez&2d`x}zO03?K490NscI9)K%F1|eo}?{NyT z5%YCoWS>P8@)c>`F2ebI>;wJq*%RDXZ|-iuDOc9b`cKYkPL6m(`!rrnqj8m!ym3t> zy$0=XS8O(~2y~1%hG52!_-9AjU^Jzz=hW9yV1_ih{;yW{uLf!>ExjAtmxR4=Ca{G& zvd6pH+bYcAK6>Nte+e@Wcdemq)C%)D2t4U^uHSh}4260dYzM%1g9oeZ*Z0N+*MXEK zd35)-u~Yg^U$Q%f&2aU6#)$pEIjDCCZtY`;5^Td}(X1pjRq;~)A6h43!}_>eV10%_ z2GBtP3;FSJAQ{3;6c5Tx40;sIXt^Lyx7T#JEFaOa1B?CcY&PP#hwrlFX2XcNV4chv zPr>WEr`>S(@;O$UiSO?M0s<_)V6^!!NQdKi5TUF>q)RLhPMi*(g{99*R=Mtj!yK!6 zYeaR7q@F~)D5K-!U!w0 zu5>fj7mJ*BL9^cxok|fX$0+H|h1YT+OYZA!EO)m{(mxXCSCS+kF1+6EmRl>V?|Hu_ z)42sU1m|C<0@W@@Yub3^!;X-lpPCOtSN|cTMX(TI(=3uGb7Wutog+zx>6PiWcpT#Y zy0S6!xv{wH&z^n?vbdYizkABNh2%zWKp!se-~_`Tlhe)p%1%SQF+qq8B82@x zP**?+y3k31$sGLMCfw-;OcD|28b(tvp2fOAr%+_kT!&((hEUo)5>d zXn>B{t2&GQcm!PW`PoKt_wnX`*meNc<<+cQ!c1+tyy59< zSW&Lo{73479HST#Ho1*>dPmpSt?`og5pIKM4VV4uwAwZRY<9jd3PjRgpJ1z-)rE*! zp{~CX51kJ@uJ_}J6u4b4{Vr|JaNW=lBg|Zd!i>Ph;}gn~;6tL% z!z|6!twkWoa+B}BVdl?rbf;5RLX=FDIL)10>+NCTZ$l=^5gYpxl@ccsaPCmA#N(%DRA7u+(C6H8sSc_5B&%YJ# z`}-9gQ^aBSepU0@*7L&^;DZnBWKE6)@e0c&9S;}SXN_ax74 z?kO)M(j}K#w99INbpUJ$01eq^Q{LPUmba}I6qd)uY`91%rdh!y6y<-uBw}ilP_hr4 z+Clqlgw=2qNyH|N`}W|A{{{m9fB=dvz)@-HjTEO}A;Sv^7lu6vzb?%dLjST1eD=Jj z+N5BuQdAoa(x_0!Wz$x-$mM3|tAQE>PHQ6Km1GBgCn9V{ViCbAASZjV@_k^@Rr(M9 zLc-2b+vuF90hNk1Rq&7X@OlGMQd}e(R4K^ya9el+Xhjfq^x(ZcVjZzM3n-Q-gYy)o zZ62R9Gs%ieN|vj<0M;{r>jv;7lxva=?Yt37MiA1K(ok7@kccd7)$chxYCTaQqgWZJ zIt7Hmxj8+6nK3DGM@IucaZZ-F6Xrad_y^~Da+dvDDt?kqfl+_S_*q8mibn}+!p@{E6y2A)b z_#LEX*pkiu!ua+@H*oiot-cnF2-Mftzuz(lj$JfBgh;lF8Rm9xm%s1+{B5I52bDmX z=pWat>6(S>;_Eg!c2{;7OffP6B1UcuT0sl{_#8N|N=)&U%k^w{Kc*%)@kTUQ9q0*6 zAH_=?T7H<&dM(F*zv!Jb)d`WZkzRX81kYIuE|^|`oyeXpre9I3lOM81PUmZu(a>AFnXXD{7g!111qpwq^ORFA81)4OzgtnV+A( z|0~!vusHdQB-xTd`g+^Bx#w%G5TlUf2fQZT<@J$-oqy%BMM!l?Qr8l-zM2i_K0Xbg z7PRnB&w&hK6{)<1?(6SIGOQo|c>^R4Km-BY^rt^HP~pYM(LGJzp%Xu&U{!?aActpr z)^1Er7I{@%yo}NgbRKb~1o(MCa1bDo7~4DhjA9NSv;Uaq7ypvg#&;XpfXcao?6oNd z2~@^(M|t8S&GsQz@RxmW_?JAyK{pT%dIG-*V0$wF<5go)X26e-`4Jc;{;5{96Vbl} zYimUJ@?S9uAa}UAnlY3T-RTCBXn;@z$P59y=Ll+@yyxcUVJ@_xEAJwaKg?u0<_I+f zON`Q`XA!Vd|&3^}P<<)tgY z1_OeNiq|(73gEC{Z_=D;WW=KbANlFA;6l_Pmj^Ck3=tE8y@mMHbRQ#` z#Ur#4gwNuyIo?-B1*eXjxPR1K-C4>;WL5|U_Y%bZ8XKR$jaRs9TyuURMWLFEx(THbkUz5pVDic>dJHV z6FK|-yAR&U9y_CkngLlQoP?#Tk62Ayu#gI9OO`=93U_a{oH_o0H-Mtn58NqGcI z2-7lco?&z`-;s|3Y}_xTN58T=wwW4vyHaX6l&`zi*KoVMq6MctDnYIf){dhRKpva9 zgTYd+b*~%h36<$55UTj&z&3ePVhTH?P}7WaeY~~>5c5*YpU$7)Awn13IDR+MHR~_$ z1j%0jcLqQ(2XIeYO`V)~S`SeqF6Y+iZtl~4z3InA8`0$gdcd2hxv zjJm0w2fpaP2@rPA7!VbPPzW)Isv#cH=R$^{Sg^>sX+SKtYoS7#DG+Yj~U%x`*B zUMx*l7RiaWQ(S1ulL9`*!XT-@hi|OOb;D%VUDqNCr#SIdv48p)=bJcnI^0lRL4dyu z2p&9{48*Z`pXJ(`(7ZHAAWMhk3I%msv(C!`|JC9-I}?0nQW_76DKUSfv>&&{yks`= z2KamZ$lRaGPj3`a_HD4O>%pT}kIs(mre^MC{HdFg6X5}V14wwd`gu>R_)}<~kZ>&t znsMGkKX+O1}G{jN*dLB zeR!poiy*yR0hbLAu`FOX*oXP~edTA4wKWd7kBf}Kxb@N>FWqhlbM*dx`d7#$%I8cr z0>3$CaxYVGbhb${zpi~u-!-!_KD*(B{Hsz#UEQ0%hkSL0m1c6JV(&>mQ9N#l0cM>O zfU*T_in9xRhs+iwq^%+RFv_Bxnc1AFRSOpyZG&wh!wlT-*#gRb<_L{}-EjW&MzhZ> zw0LLPamRI2{P_9uJrWI|i_!(8ZJOJtXLjLAlISa$uWpEV3p|3D8H*xIKHN*eI{I$Q z%Q|(L2^@VPX<3Cj6V96voW_4R;LlQ9D*f4XOi7u1v0R+En!BZ62g!rejCTkrsvtaP zFj^90{4?kKJzDKUym zR7^n>?`%AS+c_C|qR8_4sDdeNHg)(&f?wFGy^RX}TO?E3pz@0)bA*&#gIujxue$qC z$;r9whr|t&9BkV^dhQ>Zk{slCK$Z?kzS-=KyZjRpt@w5x&dxIh(zc`wNz6AbZ@QQ| zi(ZX@I7Z67A=5o(wL#vu5y#p!21e_)F3jscr#di=DKnEGvC)q z1jz^2neqP7Aqj%%HXNeSNpl>7XkgPQQ~h6K&@kcTX2vju18XF!j{daIZ0b%s@2p6O znHX)D)V8Miu6S`ZTTPc;jJQe#nen&a?gfFhXY`XjHK8E2EC57LGc+b1WrbO(g38dW^>4M@pRcU=M+EoOAF1CiuUoHw2bqzg$JK&Ji^94U zrNLDr+5^-01{ByG_z(im`pjV#LOLlcn&vwxd#CpoFAp0ht_R%5APJ@G@qj zjwOY68};)abNlt17wM@Ta$##jZTVQpcf849%UMi#!WToO@t56=9E+^N-*b|_=W;8_ z0UL+Z>?dk6gL0Q{tG=yoBUj8H!2J~4{{G9md&O<%KVNZ2 z*EXJ)PDO_L$EAU$P=XU9Wepp9#iI&u>5A~d zcdpj43I&H!faME%#xVNCfAbw%O|}T6ADetk!MSXu{73hW>C<*Fm&d#O_nM$MDZG5n zdr=XY;>Z6!f~i}EM#Z=-ofUV=n?H<@YEKlc5166(UYukzQ1`~l@w>gJ}K~b{zwOE+c@`$iO}_NYouLd z6&>4Wuo(niyZmK_62{p{%gRz3AuIk2=);9b6<-!XKh3&m8t!*8k}>NSKVMnhV-b0B zaqOFAapYM0>ne0|yD&gPfVVY&Zjc&{JU+%bZV#K;PHwG~=AkqE&X=j8dPb-lDAmP| z7xOlz)|T%s5_ifie<^7!4nrf6Vl3g5=$^w-KtHc6dAt!VmyptOR9Z7ht0ZP98A9PD z3)fY1c{l%Fq68CCRNmq7)9CnzeCC1NSnspprsvbRcRPL(O zwsAEp45*CTVhNKK#kdH$Y!;2{PMaLkSkR@3tY@%Dt}JA@67C(#kQQMr+I*Dl z|1hE$E@xP4j_!dL0bWH?WCTARtWk`hhu}Qjk>H(IUY^!3%`*LC98d-lXRUHoO2t(Q3VFQ)5{!zP8>czZTe%R~DrFOhnhiE< z{3gZ#wjM-^0+28A>AeBhfwdP$?p_N07dcTg>jo7T=3dwy=yPY^KKa}n}c8L;I=7?!H3nJ`z!J9K2~w= z9Ad)q2FwF3ptc6)L6%?phA?> zgi-ldkzCmN1k`QXo}2dM7{?GhCWPW89>zg|e+_~T^ipd1o@;I3-X0YPasSSs@3H#h z2k1MX3{VZLnCEEf#~~mek!xs@I;5L5eM!!#FSJv|euZx2Cs)4r$+buXGMZ{Iq-BDZCKue^P?Ji*JW-a3vCha&fkX3kA$_eH`<-6+>eB?0LJ>qr!?>J1?O@w_; zZK7Y~tk{|z|5<35+F*)qggeuyC_BpiIV6+-lp6KXZ|qaviic%io^@#*c5NRkYpsqR z#YGi|-fN*u;*TkID;Ey>+^KJW&3dzyqqA9fu|V@T5B`K$OFoGO>Lew|a@r=VqVr%_ zU319pp2q4C?X9!3el_~V81;kcF%s2DaZU^ytqr*^7u_)Irf*~6XK$-%_#7PWvmRA_ zOQRPd$V1BFLfP_*-e~n};ZM$OmQ-re*otr+ZQFFc1Le%9D#>nU+U~mJA-hyM zmHmwcq2KI8EJpQ&VYMuOe(Op|rsEcFmm5y<>H1qVah_)R=56P-^UC=Xc+GYgz`7(NSZ|$YlDpFv>88RNylyLAkjP|!VTIe^e z%6<*K&!HdH#tHv0il1}-JVMooFH1pZ6Ch;ejKn0g=hQQSb>gz>Sz=OAk@6KnhOkam z%f8&><0%cP4dQdBaVtfn;&OLf1Qq>odw8h-LYQiwT%68>nOT0IS%AUwhaV}B3)FB4 z6~pfBlQt;taO^6a(9s;A&E_MGl@)PHia{St;m+l=718?jV#IT} z;wNYh_1m%ChE^F<*(~EtF)I@xSWWgOghE8c<-O7&SwI=)-7M{EX$Ht}DJ>DNlKd+& z{usq?>0DZf?ls-AJQ;c5`A&01OIn+-Bt~v^bX!e}s}Ss;=m#c9`4uq!q0m=77fd)R z7@ia={L!f$A~B43dR69`LWN@6UsW;OSafoB*K`NFbyIZ7lijFTJ=2W|PSSW3ig8cT zLy1umHKrEYbG^^+7KUV#;<6YD?2t7EA$3VM7oB&V)4GFQy55Ub<(c>@zfUpHsMesA zDB<%|Q^8;#VA+x#2fM8A1D@(9*J5F}v$_A*)R~5}fvs^oB8^yraErzg zYHRI77+OmZd$py4nnvk@+Jb6DLTN-$`_-!IOi`q&2-BiPBwDwqYf#j_*NECOw$z#l zz4t!%^I6{i|9#H$yubH+I7eP_r>y-otpQL%4&;}jM%HpK_Ss>o&4Cz}zn8e5?wN~K z4l7eD!n!Z6lS-t$GYGb)j~~x+r*wuoEam2ldemTqiu|0u)d^YD%Ne@X&AkvOqwq7R zDI1ZD#)L9~AVs=GiT@H-@bfyWbTAgCk0JUaRgx)6QtmE!Xixd7*%uLeJHl-kFyqRl zTlo?44|w0@^<_icbS!S5-Wn84cG644R{6Vn!|GS!OQF)k>WdRU!vISC6cae#msy3d)j7YI_i&TUq-arqClPk@N0Ns0Fezb`ruOM%j<`aYY~ z4Od7xd^}u5=Uyx7jWW2oO#=ul8uYH%Fw!{@)6`w;vHS?^L(tC6E&8VaT*h2RhnyvO)zQd@ zf0Yw!LC(4l3eEdxsmkGFJ~}bF=X6Tdg~*&e%lkbC*H>5bGYATClCHFbI_b}MZNE;; z)(K?pL?3i7ox{mc^1H8Fu=9&prB3$nkdThzR!|aeoEzaEDtGa&Q8l{tO zS13hkF7M2l!dhG4wug(=(;C(xLWXdo8>d}lp|9Yifc9Ov34?g0ODT!y)}PD4g%5<} zX|-K6JPzy%(Pk`n`L5P*%$8#sbm1kzvf&;Y2@3=q5n1u&z0f&pBsoAfk`da1WFph* zq;dIg5W5V!_L0RjJt)`O?4_=Ii5so{3)y|VbfFC5)3v3nGBN0^O3Bw&#qZ4r4Z1)! z&8L3d``tprItmdWIwSa(>{C(9FKaWTVFlxXi2i-g^{Vx(@HO3TAb#fIuq59U&WbwJ zDL^Xc=dtYT%T9y#P^FeTv{7D#j$u^{taK?zjYNk|UXQvt+E_XKsW{Vz`Bo~Hk>?7^Z3h}jmC>Q>K=w2GM6Ok;AKm_ovR|5HGV5&oyeKAw> z>oafZPc*Ob{aRpHR^Vf7H5HOELBa4|bncLyKVzn5Y*6D-P74(1;BUGP6>@D>{I`I zEm)mLU4DU2sq^WDZrPd`Gt?axE-w96{9LpJJsFj56sXXAdaD6P*!HqL5sOGuxVLeB zN3>d6_3Q$ln-udGCv6S&JYqRg#o#*lLbt3ZrEsn+hBaWAB<3zZ)4;-&-FjC(u$Xu( z_1u-P?F?^D4s|5UH2l-oy^4-d%-WV~s;$%9nxT3^vzV?cW1rX{IZTL~;{tbldLTtYp*CPAIW(wZaQ}wM(*9sjSOja= z2f0x9CrDJhK~&egjteN7Ef+MdgYX#If|_Byggk(!+w!z-1r*4Kji-Xhk7SIiBAGRi z_}Gt()Op&Ww)8(01<)wgcQiBmsLCI%h{m6H1j)VRb;(x`o#xxktMG;e;~lN_&Aw!> zTeRE`nd3P3Z9g#p8*BaG?jgJX};p0aUAkrgQP6+n_#!rdsQ%NvM! zgshLp?Bv2WVry(o2a!K*LnE&C2K5j(;Av`NH7mPbAZg6Q{Zn2b#A#qp?ZAbkeZ#{* zCa$8+0;X2ZOlx5Pmi^*Tc2`Guf8u>H)k7pYa`Dx~5yocn+Oi*k32iPZ@d6S6LKE#U z14#P(sgK~vdVulN_UOp>Af9jAKFzHZz;25&A7=oJpR%7kKg+ES(QJVFA$u1mc#<|0 z=-}cG7>(Fy!-g-j_EJ;Ta9BoE>Xex>U}XI$Dm5;8$exKk#=E8HJ5)jCM(4d}S|9TM zj|7Q|X#g7zTZ|(gCrlO4AUScse*_r39iu^p)cyi53sOgv!$P@60P}n`W z=CvPcTp_Jc)6Ml}nGf}%|1WRqO)CN!5~9(!hf=hHcwe&QIiM}wfT4I;{K8KO(o(=@ zTxim!{POmZX9JeXNz&$TOqcgcc;6N`pilUSVz8wlYum2&M;`rT?9p_4CN!xW>f0%M z%=C%G!^NFis#Tdsj&1 | head -n 1 | grep -q '"17' +} + +if ! is_java17_home "$JAVA_HOME" ; then + for candidate in /usr/lib/jvm/java-17-openjdk-amd64 /usr/lib/jvm/java-17-openjdk /usr/lib/jvm/jdk-17 /usr/lib/jvm/*17*; do + if is_java17_home "$candidate" ; then + JAVA_HOME="$candidate" + export JAVA_HOME + break + fi + done +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" From 77d0891406a280506f712c827a1d654424c6015d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 13:07:49 +0300 Subject: [PATCH 4/8] Fix iOS arch mismatch + dispatch JS events on peersContainer iOS build was failing because IOSSimd.m uses ARM NEON intrinsics guarded by `#if defined(__aarch64__)` and a `generic/platform=iOS Simulator` destination triggers a universal build whose x86_64 slice has no `uint32x4_t` etc. Mirror what run-ios-ui-tests.sh does for hellocodenameone: detect host arch via `uname -m`, force `ARCHS=$HOST_ARCH ONLY_ACTIVE_ARCH=YES` and pin `-sdk iphonesimulator -configuration Debug`. JS Playwright driver: `page.mouse.click` was missing because the CN1 JS port registers pointer listeners on a sibling `

`, not on `` (which has `pointer-events:none`). Page-level clicks land on body and never reach the listener. Dispatch synthetic `MouseEvent`s on `#cn1-peers-container` via `page.evaluate` so the CN1 dispatcher actually sees mousedown / mousemove / mouseup -- still exercises every CN1 listener that a PR #5003-class bug would break. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/input-validation.yml | 31 ++++++--- .../drivers/playwright-driver.mjs | 69 +++++++++++++++---- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml index 055fea090c..22057b6b6a 100644 --- a/.github/workflows/input-validation.yml +++ b/.github/workflows/input-validation.yml @@ -151,17 +151,28 @@ jobs: SCHEME='${{ steps.build-ios-app.outputs.scheme }}' DERIVED_DATA="${{ runner.temp }}/cn1iv-dd" rm -rf "$DERIVED_DATA" - if [[ "$WORKSPACE" == *.xcworkspace ]]; then - xcodebuild -workspace "$WORKSPACE" -scheme "$SCHEME" \ - -destination 'generic/platform=iOS Simulator' \ - -derivedDataPath "$DERIVED_DATA" \ - CODE_SIGNING_ALLOWED=NO build - else - xcodebuild -project "$WORKSPACE" -scheme "$SCHEME" \ - -destination 'generic/platform=iOS Simulator' \ - -derivedDataPath "$DERIVED_DATA" \ - CODE_SIGNING_ALLOWED=NO build + # Force ARCHS to match the host. IOSSimd.m uses ARM NEON intrinsics + # under #if defined(__aarch64__) -- without ONLY_ACTIVE_ARCH=YES the + # generic simulator destination triggers a universal build whose + # x86_64 slice fails to compile. Matches what run-ios-ui-tests.sh + # does for the hellocodenameone build. + HOST_ARCH="$(uname -m)" + case "$HOST_ARCH" in + arm64|x86_64) BUILD_ARCH="$HOST_ARCH" ;; + *) BUILD_ARCH=arm64 ;; + esac + XCB_CONTAINER_FLAG="-workspace" + if [[ "$WORKSPACE" != *.xcworkspace ]]; then + XCB_CONTAINER_FLAG="-project" fi + xcodebuild $XCB_CONTAINER_FLAG "$WORKSPACE" -scheme "$SCHEME" \ + -sdk iphonesimulator \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$DERIVED_DATA" \ + ARCHS=$BUILD_ARCH ONLY_ACTIVE_ARCH=YES \ + EXCLUDED_ARCHS="armv7 armv7s" \ + CODE_SIGNING_ALLOWED=NO build APP_BUNDLE="$(find "$DERIVED_DATA/Build/Products" -maxdepth 3 -type d -name "${SCHEME}.app" | head -n 1)" if [ -z "$APP_BUNDLE" ]; then echo "Failed to locate .app bundle under $DERIVED_DATA" >&2 diff --git a/scripts/input-validation-app/drivers/playwright-driver.mjs b/scripts/input-validation-app/drivers/playwright-driver.mjs index d43e42573d..a7d57e6c5e 100644 --- a/scripts/input-validation-app/drivers/playwright-driver.mjs +++ b/scripts/input-validation-app/drivers/playwright-driver.mjs @@ -79,27 +79,68 @@ try { process.exit(1); } - // Tap: click the centre of the form. - await page.mouse.click(viewport.width / 2, viewport.height / 2); + // Dump the DOM topology once so a failure leaves a breadcrumb. The CN1 JS + // port creates a
sibling of the + // and registers all pointer listeners on + // the peersContainer (not on the canvas itself, which has pointer-events: + // none). Page-level mouse clicks miss because the canvas is in front and + // pointer-events:none routes the click to body rather than the sibling + // peersContainer. We therefore dispatch synthetic events on the + // peersContainer directly -- this still exercises the entire CN1 listener + // chain that PR #5003-class bugs would break. + const layout = await page.evaluate(() => { + const canvas = document.querySelector('#codenameone-canvas'); + const peers = document.querySelector('#cn1-peers-container'); + const r = canvas ? canvas.getBoundingClientRect() : null; + return { + hasCanvas: !!canvas, + hasPeersContainer: !!peers, + canvasRect: r ? { x: r.x, y: r.y, width: r.width, height: r.height } : null, + }; + }); + record(`layout:${JSON.stringify(layout)}`); + if (!layout.hasPeersContainer) { + console.error('CN1 peers container not found; JS port did not initialise.'); + process.exit(1); + } + const rect = layout.canvasRect; + const px = (frac) => rect.x + rect.width * frac; + const py = (frac) => rect.y + rect.height * frac; + + async function fireMouse(type, x, y) { + await page.evaluate(({ type, x, y }) => { + const target = document.querySelector('#cn1-peers-container'); + const evt = new MouseEvent(type, { + bubbles: true, cancelable: true, view: window, + clientX: x, clientY: y, button: 0, buttons: type === 'mouseup' ? 0 : 1, + }); + target.dispatchEvent(evt); + }, { type, x, y }); + } + + // Tap: down + up at the form centre. + await fireMouse('mousedown', px(0.5), py(0.5)); + await page.waitForTimeout(50); + await fireMouse('mouseup', px(0.5), py(0.5)); await waitFor('CN1IV:EVENT:tap', 5_000); - // Drag: sweep horizontally so we exceed the 3-sample floor in DragStep. - // page.mouse.move with steps=N emits N mousemove events -> CN1 sees N - // pointerDragged calls between the down/up pair. + // Drag: down at left, several mousemoves across, up at right. Need ≥ 3 + // pointerDragged samples for DragStep to fire. await waitFor('CN1IV:READY:drag', 5_000); - await page.mouse.move(viewport.width * 0.2, viewport.height * 0.55); - await page.mouse.down(); - await page.mouse.move(viewport.width * 0.8, viewport.height * 0.55, { steps: 12 }); - await page.mouse.up(); + await fireMouse('mousedown', px(0.2), py(0.55)); + for (let i = 1; i <= 10; i++) { + const frac = 0.2 + (0.6 * i / 10); + await fireMouse('mousemove', px(frac), py(0.55)); + await page.waitForTimeout(20); + } + await fireMouse('mouseup', px(0.8), py(0.55)); await waitFor('CN1IV:EVENT:drag', 5_000); - // Long-press: mousedown, hold, mouseup. CN1's long-press threshold is - // ~1s by default; 1500ms gives us comfortable headroom. + // Long-press: mousedown, hold ~1.5s, mouseup. CN1's threshold is ~1s. await waitFor('CN1IV:READY:longpress', 5_000); - await page.mouse.move(viewport.width / 2, viewport.height / 2); - await page.mouse.down(); + await fireMouse('mousedown', px(0.5), py(0.5)); await page.waitForTimeout(1500); - await page.mouse.up(); + await fireMouse('mouseup', px(0.5), py(0.5)); await waitFor('CN1IV:EVENT:longpress', 5_000); await waitFor('CN1IV:SUITE:FINISHED', 5_000); From d52d4e7ab12b74335b1af6a32ea27bf573e7072a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 14:38:01 +0300 Subject: [PATCH 5/8] Pin iOS sim by availability; broaden JS dispatch to touch + pointer iOS: previous run-ios.sh hard-coded `iPhone 17 Pro` which only exists on Xcode 26 -- the macos-15 runner default Xcode is 16.4, top device is iPhone 16. Auto-pick the newest available iPhone runtime and fall back through the list if the user-supplied CN1IV_DEVICE_NAME isn't installed. JS Playwright driver: add a layout pre-flight that fires its own mousedown probe on `#cn1-peers-container` to confirm dispatchEvent actually reaches the listener (rules out "synthetic events are silently dropped" before we blame CN1). Also fire PointerEvent + TouchEvent variants alongside MouseEvent in case the JS port's pointer dispatcher only listens to one channel. Same gesture flow, just multi-modal. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../drivers/playwright-driver.mjs | 88 +++++++++++++++---- .../input-validation-app/drivers/run-ios.sh | 53 ++++++++--- 2 files changed, 112 insertions(+), 29 deletions(-) diff --git a/scripts/input-validation-app/drivers/playwright-driver.mjs b/scripts/input-validation-app/drivers/playwright-driver.mjs index a7d57e6c5e..cca9c96c57 100644 --- a/scripts/input-validation-app/drivers/playwright-driver.mjs +++ b/scripts/input-validation-app/drivers/playwright-driver.mjs @@ -79,23 +79,36 @@ try { process.exit(1); } - // Dump the DOM topology once so a failure leaves a breadcrumb. The CN1 JS - // port creates a
sibling of the - // and registers all pointer listeners on - // the peersContainer (not on the canvas itself, which has pointer-events: - // none). Page-level mouse clicks miss because the canvas is in front and - // pointer-events:none routes the click to body rather than the sibling - // peersContainer. We therefore dispatch synthetic events on the - // peersContainer directly -- this still exercises the entire CN1 listener - // chain that PR #5003-class bugs would break. + // Dump the DOM topology and verify the dispatch path before driving any + // gestures. The CN1 JS port registers pointer listeners on a + //
sibling of the canvas, so page-level + // clicks miss the listener. We dispatch synthetic events on the + // peersContainer directly. Pre-flight checks confirm dispatch is wired + // before we blame a CN1 regression for a missing event. const layout = await page.evaluate(() => { const canvas = document.querySelector('#codenameone-canvas'); const peers = document.querySelector('#cn1-peers-container'); const r = canvas ? canvas.getBoundingClientRect() : null; + const peersStyle = peers ? window.getComputedStyle(peers) : null; + let selfTestFired = false; + if (peers) { + const probe = (e) => { selfTestFired = true; }; + peers.addEventListener('mousedown', probe, true); + const evt = new MouseEvent('mousedown', { + bubbles: true, cancelable: true, view: window, + clientX: 10, clientY: 10, button: 0, buttons: 1, + }); + peers.dispatchEvent(evt); + peers.removeEventListener('mousedown', probe, true); + } return { hasCanvas: !!canvas, hasPeersContainer: !!peers, canvasRect: r ? { x: r.x, y: r.y, width: r.width, height: r.height } : null, + peersDisplay: peersStyle ? peersStyle.display : null, + peersPointerEvents: peersStyle ? peersStyle.pointerEvents : null, + peersPosition: peersStyle ? peersStyle.position : null, + selfTestFired, }; }); record(`layout:${JSON.stringify(layout)}`); @@ -107,39 +120,82 @@ try { const px = (frac) => rect.x + rect.width * frac; const py = (frac) => rect.y + rect.height * frac; + // Dispatch mouse + pointer + touch variants for each gesture step. The CN1 + // JS port registers listeners on both mousedown/pointerdown and + // touchstart/touchend; firing all variants maximises the chance that one + // reaches the actual handler regardless of which path the port currently + // routes through. async function fireMouse(type, x, y) { await page.evaluate(({ type, x, y }) => { const target = document.querySelector('#cn1-peers-container'); - const evt = new MouseEvent(type, { + const isUp = type === 'mouseup'; + const mouse = new MouseEvent(type, { bubbles: true, cancelable: true, view: window, - clientX: x, clientY: y, button: 0, buttons: type === 'mouseup' ? 0 : 1, + clientX: x, clientY: y, button: 0, buttons: isUp ? 0 : 1, }); - target.dispatchEvent(evt); + target.dispatchEvent(mouse); + const pointerType = type === 'mousedown' ? 'pointerdown' + : type === 'mouseup' ? 'pointerup' + : 'pointermove'; + try { + const ptr = new PointerEvent(pointerType, { + bubbles: true, cancelable: true, view: window, + clientX: x, clientY: y, button: 0, buttons: isUp ? 0 : 1, + pointerId: 1, pointerType: 'mouse', isPrimary: true, + }); + target.dispatchEvent(ptr); + } catch (_) {} + }, { type, x, y }); + } + + async function fireTouch(type, x, y) { + await page.evaluate(({ type, x, y }) => { + const target = document.querySelector('#cn1-peers-container'); + try { + const touch = new Touch({ + identifier: 1, target, clientX: x, clientY: y, + }); + const list = (type === 'touchend') ? [] : [touch]; + const evt = new TouchEvent(type, { + bubbles: true, cancelable: true, view: window, + touches: list, targetTouches: list, + changedTouches: [touch], + }); + target.dispatchEvent(evt); + } catch (_) {} }, { type, x, y }); } - // Tap: down + up at the form centre. + // Tap: down + up at the form centre. Touch first (matches real mobile), + // then mouse as belt-and-braces. + await fireTouch('touchstart', px(0.5), py(0.5)); await fireMouse('mousedown', px(0.5), py(0.5)); - await page.waitForTimeout(50); + await page.waitForTimeout(80); + await fireTouch('touchend', px(0.5), py(0.5)); await fireMouse('mouseup', px(0.5), py(0.5)); await waitFor('CN1IV:EVENT:tap', 5_000); - // Drag: down at left, several mousemoves across, up at right. Need ≥ 3 + // Drag: down at left, several moves across, up at right. Need >= 3 // pointerDragged samples for DragStep to fire. await waitFor('CN1IV:READY:drag', 5_000); + await fireTouch('touchstart', px(0.2), py(0.55)); await fireMouse('mousedown', px(0.2), py(0.55)); for (let i = 1; i <= 10; i++) { const frac = 0.2 + (0.6 * i / 10); + await fireTouch('touchmove', px(frac), py(0.55)); await fireMouse('mousemove', px(frac), py(0.55)); await page.waitForTimeout(20); } + await fireTouch('touchend', px(0.8), py(0.55)); await fireMouse('mouseup', px(0.8), py(0.55)); await waitFor('CN1IV:EVENT:drag', 5_000); - // Long-press: mousedown, hold ~1.5s, mouseup. CN1's threshold is ~1s. + // Long-press: down, hold ~1.5s, up. CN1's threshold is ~1s. await waitFor('CN1IV:READY:longpress', 5_000); + await fireTouch('touchstart', px(0.5), py(0.5)); await fireMouse('mousedown', px(0.5), py(0.5)); await page.waitForTimeout(1500); + await fireTouch('touchend', px(0.5), py(0.5)); await fireMouse('mouseup', px(0.5), py(0.5)); await waitFor('CN1IV:EVENT:longpress', 5_000); diff --git a/scripts/input-validation-app/drivers/run-ios.sh b/scripts/input-validation-app/drivers/run-ios.sh index 804f0c1b13..16e04db72d 100755 --- a/scripts/input-validation-app/drivers/run-ios.sh +++ b/scripts/input-validation-app/drivers/run-ios.sh @@ -49,31 +49,58 @@ if [ -z "$BUNDLE_ID" ]; then fi iv_log "Bundle id: $BUNDLE_ID" -DEVICE_NAME="${CN1IV_DEVICE_NAME:-iPhone 17 Pro}" +DEVICE_NAME="${CN1IV_DEVICE_NAME:-}" DEVICE_RUNTIME="${CN1IV_DEVICE_RUNTIME:-}" -iv_log "Locating simulator: $DEVICE_NAME" -SIM_UDID="" -while IFS=$'\t' read -r udid name state runtime; do - if [ "$name" = "$DEVICE_NAME" ] && { [ -z "$DEVICE_RUNTIME" ] || [ "$runtime" = "$DEVICE_RUNTIME" ]; }; then - SIM_UDID="$udid" - break - fi -done < <(xcrun simctl list devices available -j \ - | python3 -c ' +# Build a sorted (name, runtime, udid) list of available simulators. Newer +# iOS runtimes sort last so we pick them by default. +read_devices() { + xcrun simctl list devices available -j \ + | python3 -c ' import json, sys data = json.load(sys.stdin) +rows = [] for runtime, devs in data.get("devices", {}).items(): + if "iOS-" not in runtime: + continue for d in devs: if d.get("isAvailable"): - print(f"{d[\"udid\"]}\t{d[\"name\"]}\t{d[\"state\"]}\t{runtime}") -') + rows.append((runtime, d["name"], d["udid"])) +rows.sort() +for r in rows: + print("\t".join(r)) +' +} + +SIM_UDID="" +if [ -n "$DEVICE_NAME" ]; then + iv_log "Locating simulator by name: $DEVICE_NAME" + while IFS=$'\t' read -r runtime name udid; do + if [ "$name" = "$DEVICE_NAME" ] && { [ -z "$DEVICE_RUNTIME" ] || [ "$runtime" = "$DEVICE_RUNTIME" ]; }; then + SIM_UDID="$udid" + break + fi + done < <(read_devices) +fi + +if [ -z "$SIM_UDID" ]; then + # Fall back to the newest available iPhone (any model). XCode 16.4 only has + # iPhone 16; XCode 26 has iPhone 17. We'd rather adapt than fail-fast on a + # CI runner that doesn't have the exact device name we'd prefer. + iv_log "No exact device match -- picking the newest available iPhone" + while IFS=$'\t' read -r runtime name udid; do + case "$name" in + iPhone*) SIM_UDID="$udid"; DEVICE_NAME="$name"; DEVICE_RUNTIME="$runtime" ;; + esac + done < <(read_devices) +fi if [ -z "$SIM_UDID" ]; then - iv_log "No simulator matched name=$DEVICE_NAME runtime=$DEVICE_RUNTIME" >&2 + iv_log "No iOS simulator available on this host" >&2 xcrun simctl list devices available >&2 || true exit 3 fi +iv_log "Selected simulator: $DEVICE_NAME ($DEVICE_RUNTIME)" iv_log "Using simulator $SIM_UDID" # Boot the simulator if needed. `bootstatus -b` blocks until SpringBoard is up. From 23d455c26b77f787655d2641869cd3c37fa24fce Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 18:05:33 +0300 Subject: [PATCH 6/8] Mark input-validation jobs advisory; capture iOS xcresult Both jobs run, build, install, launch -- but the actual gesture-dispatch endpoints turn out to need port-side work: * iOS: XCUITest application-only mode (no host-app) errors with the opaque `** TEST FAILED **` under Xcode 16.4. The runner build + simctl install + log-stream path is verified. Capture the .xcresult and run xcresulttool against it so the artifact has the real failure reason for post-hoc diagnosis rather than a generic xcodebuild line. * JavaScript: the layout dump shows synthetic mousedown reaches main- thread listeners we add (`selfTestFired:true`) but the CN1 JS port registers its pointer listeners through a host-bridge proxy from inside a Web Worker; synthetic events don't traverse that proxy the way real OS events do. End-to-end input validation here needs JS-port-side work. Mark both jobs `continue-on-error: true` so the infrastructure lands without gating PRs on issues that are about the ports, not about this pipeline. README updated with the current state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/input-validation.yml | 15 +++++++++++++++ scripts/input-validation-app/README.adoc | 15 +++++++++++++++ .../input-validation-app/drivers/run-ios.sh | 18 +++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml index 22057b6b6a..7676ebe1f4 100644 --- a/.github/workflows/input-validation.yml +++ b/.github/workflows/input-validation.yml @@ -44,6 +44,13 @@ jobs: contents: read runs-on: macos-15 timeout-minutes: 35 + # Advisory while we iterate on XCUITest application-only mode under Xcode + # 16.4 -- the build/install/launch path is verified, but the test runner + # exits with an opaque `** TEST FAILED **` when no host-app target is + # configured. Keeping the job running so the artifact stream stays warm + # and a regression in the build chain still surfaces. Once the runner is + # green we flip this off. + continue-on-error: true concurrency: group: mac-ci-${{ github.workflow }}-ios-${{ github.ref_name }} cancel-in-progress: true @@ -205,6 +212,14 @@ jobs: contents: read runs-on: ubuntu-latest timeout-minutes: 25 + # Advisory while the CN1 JavaScript port's Worker-based input dispatch is + # under investigation. Synthetic DOM events reach listeners on the main + # thread (verified via `selfTestFired:true` in the layout dump artifact) + # but don't propagate through the host-bridge proxy that registers + # peersContainer listeners from inside the Web Worker. End-to-end input + # validation here needs JS-port-side work; for now the job exercises + # bundle build + serve + suite launch and uploads diagnostics. + continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} diff --git a/scripts/input-validation-app/README.adoc b/scripts/input-validation-app/README.adoc index 1cd62742e6..b225bff802 100644 --- a/scripts/input-validation-app/README.adoc +++ b/scripts/input-validation-app/README.adoc @@ -118,6 +118,21 @@ cache as the screenshot-based scripts-ios pipeline; the JavaScript job boots ParparVM + JavaScriptPort, bundles to a zip, serves it via `python3 -m http.server`, and drives Chromium against `localhost:8080`. +Both jobs are currently marked `continue-on-error: true` while the +input-dispatch ends of each port are stabilised: + +* **iOS**: XCUITest application-only mode (no host-app target) exits + with an opaque `** TEST FAILED **` under Xcode 16.4. The build / + install / launch / log-stream path is verified; the .xcresult + bundle is uploaded as an artifact for post-hoc inspection. +* **JavaScript**: synthetic DOM events reach main-thread listeners + (the driver's `layout:{...selfTestFired:true}` artifact confirms this) + but the CN1 JS port runs Java in a Web Worker and registers its + pointer listeners through a host-bridge proxy; synthetic events from + the test driver don't propagate through that proxy the way real OS + events do. This needs JS-port-side work before end-to-end input flow + can be asserted. + The underlying build scripts (`scripts/build-ios-app.sh` and `scripts/build-javascript-port-hellocodenameone.sh`) accept a `CN1_APP_DIR=scripts/input-validation-app` override so the same scripts diff --git a/scripts/input-validation-app/drivers/run-ios.sh b/scripts/input-validation-app/drivers/run-ios.sh index 16e04db72d..ca97108023 100755 --- a/scripts/input-validation-app/drivers/run-ios.sh +++ b/scripts/input-validation-app/drivers/run-ios.sh @@ -135,13 +135,18 @@ iv_log "Generating XCUITest project via xcodegen" # Run the XCUITest suite. CN1IV_BUNDLE_ID tells the Swift code which app to # attach to; CN1IV_STEP_DELAY_SEC lets us slow the inter-gesture wait on -# heavily loaded CI runners. +# heavily loaded CI runners. `-resultBundlePath` captures the .xcresult so +# we can extract the actual test failure reason post-hoc (without it, +# xcodebuild just prints `** TEST FAILED **`). +XCRESULT_BUNDLE="$ARTIFACTS_DIR/test.xcresult" +rm -rf "$XCRESULT_BUNDLE" iv_log "Running XCUITest" set +e xcodebuild test \ -project "$TESTS_DIR/CN1InputValidationUITests.xcodeproj" \ -scheme CN1InputValidationUITests \ -destination "platform=iOS Simulator,id=$SIM_UDID" \ + -resultBundlePath "$XCRESULT_BUNDLE" \ CN1IV_BUNDLE_ID="$BUNDLE_ID" \ CODE_SIGNING_ALLOWED=NO \ | tee -a "$XCODEBUILD_LOG" @@ -149,6 +154,17 @@ XCB_RC=${PIPESTATUS[0]} set -e iv_log "xcodebuild test exit=$XCB_RC" +# Extract the human-readable failure summary if the result bundle is present +# so the artifact upload has something searchable beyond the opaque +# "** TEST FAILED **" line in xcodebuild-test.log. +if [ -d "$XCRESULT_BUNDLE" ]; then + iv_log "Extracting xcresult diagnostics" + xcrun xcresulttool get test-results summary --path "$XCRESULT_BUNDLE" --format json \ + > "$ARTIFACTS_DIR/xcresult-summary.json" 2>/dev/null || true + xcrun xcresulttool get log --type action --path "$XCRESULT_BUNDLE" \ + > "$ARTIFACTS_DIR/xcresult-action.log" 2>/dev/null || true +fi + # Give the log stream a beat to flush the final CN1IV:SUITE:FINISHED line. sleep 2 cleanup From 9b0cb16fbf194b951134337e6cc2435fa12b33a3 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 19:55:33 +0300 Subject: [PATCH 7/8] Drop JS job; fix iOS XCUITest with a host-app stub Two changes: 1. Drop the JavaScript job entirely. The CN1 JS port registers its pointer listeners through a host-bridge proxy from inside a Web Worker; synthetic browser events on the main thread don't traverse that proxy the way real OS events do. The diagnostic from the previous run confirms this (the driver's layout self-test fires its own listener -- `selfTestFired:true` -- but CN1's listeners never fire). End-to-end input validation on JS needs port-side changes that don't belong in this PR. Removing the job, the JS module, the Playwright driver, and reverting the JS build-script generalisation keeps the diff focused. 2. Fix the iOS XCUITest by giving it a proper host-app target. Application-only UI testing (TEST_TARGET_NAME="" + USES_XCTRUNNER) was exiting with an opaque `** TEST FAILED **` under Xcode 16.4 -- that mode is finicky and not how the standard XCUITest pipeline works. The XcodeGen project now declares a minimal `HostStub` target (a UIApplicationMain that does nothing) and points TEST_TARGET_NAME at it. The UI test code is unchanged -- it still attaches to the already-installed CN1 app by bundle id via `XCUIApplication(bundleIdentifier:)`. The stub is purely there so the XCUITest runner has a regular host to launch into. `continue-on-error: true` removed -- it was masking failure, not fixing anything. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/input-validation.yml | 185 +------------- .../build-javascript-port-hellocodenameone.sh | 55 ++-- scripts/input-validation-app/README.adoc | 100 +++----- .../inputvalidation/InputValidationApp.java | 9 +- .../drivers/playwright-driver.mjs | 236 ------------------ .../input-validation-app/drivers/run-js.sh | 36 --- .../ios-tests/HostStub/HostStubApp.swift | 26 ++ .../ios-tests/project.yml | 43 +++- .../input-validation-app/javascript/pom.xml | 61 ----- scripts/input-validation-app/pom.xml | 12 - 10 files changed, 119 insertions(+), 644 deletions(-) delete mode 100644 scripts/input-validation-app/drivers/playwright-driver.mjs delete mode 100755 scripts/input-validation-app/drivers/run-js.sh create mode 100644 scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift delete mode 100644 scripts/input-validation-app/javascript/pom.xml diff --git a/.github/workflows/input-validation.yml b/.github/workflows/input-validation.yml index 7676ebe1f4..d09ab8e7c8 100644 --- a/.github/workflows/input-validation.yml +++ b/.github/workflows/input-validation.yml @@ -1,12 +1,17 @@ name: Input validation gesture suite -# Fast-running multi-platform suite that asserts physical taps / drags / -# long-presses reach Component listeners end-to-end. Built to catch -# regressions in the input chain (the class of bug PR #5003 fixed -- a -# window-level UITapGestureRecognizer eating every tap was invisible to the -# existing screenshot-only tests because none of them actually depended on a -# touch event firing). Each platform job is independent; failure of one -# doesn't cancel the others. +# Fast-running iOS suite that asserts physical taps / drags / long-presses +# reach Component listeners end-to-end. Built to catch regressions in the +# input chain (the class of bug PR #5003 fixed -- a window-level +# UITapGestureRecognizer eating every tap was invisible to the existing +# screenshot-only tests because none of them actually depended on a touch +# event firing). +# +# Only iOS for now. The JavaScript port wires its pointer listeners from +# inside a Web Worker via a host-bridge proxy; synthetic events from the +# test driver don't traverse that proxy the way real OS events would, so +# end-to-end input validation on JS needs port-side work first. See +# scripts/input-validation-app/README.adoc for the "not yet covered" list. on: pull_request: @@ -14,21 +19,17 @@ on: - '.github/workflows/input-validation.yml' - 'scripts/input-validation-app/**' - 'scripts/build-ios-app.sh' - - 'scripts/build-javascript-port-hellocodenameone.sh' - 'CodenameOne/src/com/codename1/ui/Component.java' - 'CodenameOne/src/com/codename1/ui/Form.java' - 'CodenameOne/src/com/codename1/ui/Button.java' - 'Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m' - 'Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m' - - 'Ports/JavaScriptPort/**' - - 'vm/ByteCodeTranslator/src/javascript/**' push: branches: [ master ] paths: - '.github/workflows/input-validation.yml' - 'scripts/input-validation-app/**' - 'scripts/build-ios-app.sh' - - 'scripts/build-javascript-port-hellocodenameone.sh' workflow_dispatch: {} jobs: @@ -44,13 +45,6 @@ jobs: contents: read runs-on: macos-15 timeout-minutes: 35 - # Advisory while we iterate on XCUITest application-only mode under Xcode - # 16.4 -- the build/install/launch path is verified, but the test runner - # exits with an opaque `** TEST FAILED **` when no host-app target is - # configured. Keeping the job running so the artifact stream stays warm - # and a regression in the build chain still surfaces. Once the runner is - # green we flip this off. - continue-on-error: true concurrency: group: mac-ci-${{ github.workflow }}-ios-${{ github.ref_name }} cancel-in-progress: true @@ -206,158 +200,3 @@ jobs: path: artifacts/input-validation-ios if-no-files-found: warn retention-days: 14 - - javascript: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 25 - # Advisory while the CN1 JavaScript port's Worker-based input dispatch is - # under investigation. Synthetic DOM events reach listeners on the main - # thread (verified via `selfTestFired:true` in the layout dump artifact) - # but don't propagate through the host-bridge proxy that registers - # peersContainer listeners from inside the Web Worker. End-to-end input - # validation here needs JS-port-side work; for now the job exercises - # bundle build + serve + suite launch and uploads diagnostics. - continue-on-error: true - env: - GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} - GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} - ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/input-validation-js - CN1_APP_DIR: scripts/input-validation-app - steps: - - uses: actions/checkout@v4 - - - name: Set TMPDIR - run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - - name: Cache codenameone-tools - uses: actions/cache@v4 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ hashFiles('scripts/setup-workspace.sh') }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - - name: Set up Java 8 for ParparVM - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '8' - cache: 'maven' - - - name: Prepare Codename One binaries for Maven plugin - run: | - mkdir -p ~/.codenameone - cp maven/UpdateCodenameOne.jar ~/.codenameone/ - - - name: Build ParparVM compiler bundle - run: mvn -B -f maven/pom.xml -pl parparvm -am -DskipTests -Dmaven.javadoc.skip=true package - - - name: Set up Java 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - cache: 'maven' - - - name: Set up Node 20 - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Cache npm modules - uses: actions/cache@v4 - with: - path: scripts/node_modules - key: ${{ runner.os }}-scripts-npm-playwright-v1 - restore-keys: | - ${{ runner.os }}-scripts-npm- - - - name: Cache Playwright browsers - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-chromium-v2 - restore-keys: | - ${{ runner.os }}-playwright- - - - name: Install Playwright Chromium - run: | - cd scripts - npm init -y 2>/dev/null || true - npm install playwright - npx playwright install-deps chromium - npx playwright install chromium - - - name: Install Xvfb for headless Java AWT - run: sudo apt-get update && sudo apt-get install -y xvfb - - - name: Setup workspace - run: xvfb-run ./scripts/setup-workspace.sh -q -DskipTests - - - name: Build input-validation JavaScript port bundle - id: build-js-bundle - run: | - set -euo pipefail - export JAVA_HOME="${JAVA_HOME_17_X64}" - export PATH="$JAVA_HOME/bin:$PATH" - mkdir -p "${ARTIFACTS_DIR}" - OUTPUT_ZIP="${ARTIFACTS_DIR}/cn1iv-javascript-port.zip" - # xvfb-run is required because the CSS compiler in the CN1 maven - # plugin pulls in java.awt during the package phase even though - # this app has no CSS theme. - SKIP_PARPARVM_BUILD=1 xvfb-run \ - ./scripts/build-javascript-port-hellocodenameone.sh "$OUTPUT_ZIP" - echo "bundle=$OUTPUT_ZIP" >> "$GITHUB_OUTPUT" - - - name: Serve the JS bundle on localhost:8080 - id: serve-js - run: | - set -euo pipefail - SERVE_DIR="${{ runner.temp }}/cn1iv-serve" - rm -rf "$SERVE_DIR" - mkdir -p "$SERVE_DIR" - unzip -q '${{ steps.build-js-bundle.outputs.bundle }}' -d "$SERVE_DIR" - # ByteCodeTranslator emits the playable bundle under -js/ - INDEX_DIR="$(find "$SERVE_DIR" -maxdepth 2 -type f -name index.html -print -quit | xargs -n1 dirname)" - if [ -z "$INDEX_DIR" ]; then - echo "No index.html found inside $SERVE_DIR" >&2 - find "$SERVE_DIR" -maxdepth 3 -print >&2 || true - exit 1 - fi - (cd "$INDEX_DIR" && python3 -m http.server 8080 > "${ARTIFACTS_DIR}/http-server.log" 2>&1) & - SERVER_PID=$! - echo "server_pid=$SERVER_PID" >> "$GITHUB_OUTPUT" - # Wait for the server to actually accept connections before driving. - for i in $(seq 1 30); do - if curl -sf "http://127.0.0.1:8080/" -o /dev/null; then - break - fi - sleep 1 - done - - - name: Drive gestures via Playwright - env: - CN1IV_URL: http://127.0.0.1:8080/ - run: | - mkdir -p "$ARTIFACTS_DIR" - # Use the Playwright install under scripts/node_modules from the - # earlier step so we don't redownload. - export NODE_PATH="${{ github.workspace }}/scripts/node_modules" - ./scripts/input-validation-app/drivers/run-js.sh "$CN1IV_URL" - timeout-minutes: 10 - - - name: Stop http.server - if: always() - run: | - kill ${{ steps.serve-js.outputs.server_pid }} 2>/dev/null || true - - - name: Upload JS artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: input-validation-js - path: artifacts/input-validation-js - if-no-files-found: warn - retention-days: 14 diff --git a/scripts/build-javascript-port-hellocodenameone.sh b/scripts/build-javascript-port-hellocodenameone.sh index 46b6f428e9..1824b2bd07 100755 --- a/scripts/build-javascript-port-hellocodenameone.sh +++ b/scripts/build-javascript-port-hellocodenameone.sh @@ -26,31 +26,11 @@ fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# CN1_APP_DIR (relative to repo root) selects which CN1 Maven project to bundle -# for the browser. Default keeps the original hellocodenameone behaviour so -# existing callers stay green; pipelines that target a different app set -# CN1_APP_DIR=scripts/. -APP_REL_DIR="${CN1_APP_DIR:-scripts/hellocodenameone}" -HELLO_ROOT="$REPO_ROOT/$APP_REL_DIR" +HELLO_ROOT="$REPO_ROOT/scripts/hellocodenameone" COMMON_ROOT="$HELLO_ROOT/common" PORT_ROOT="$REPO_ROOT/Ports/JavaScriptPort" PARPARVM_ROOT="$REPO_ROOT/maven/parparvm" - -# Read packageName / mainName from the app's CN1 settings so the generated -# launcher imports the right class and the translator gets the right entry -# point. Allow callers to override via env for unusual layouts. -CN1_SETTINGS_FILE="$COMMON_ROOT/codenameone_settings.properties" -if [ -f "$CN1_SETTINGS_FILE" ]; then - MAIN_NAME_FROM_SETTINGS="$(awk -F= '/^codename1.mainName=/{print $2; exit}' "$CN1_SETTINGS_FILE" | tr -d '\r')" - PACKAGE_NAME_FROM_SETTINGS="$(awk -F= '/^codename1.packageName=/{print $2; exit}' "$CN1_SETTINGS_FILE" | tr -d '\r')" -fi -APP_MAIN_NAME="${CN1_APP_MAIN_NAME:-${MAIN_NAME_FROM_SETTINGS:-HelloCodenameOne}}" -APP_PACKAGE_NAME="${CN1_APP_PACKAGE_NAME:-${PACKAGE_NAME_FROM_SETTINGS:-com.codenameone.examples.hellocodenameone}}" -APP_BUNDLE_BASENAME="${CN1_APP_BUNDLE_BASENAME:-$(echo "$APP_MAIN_NAME" | tr '[:upper:]' '[:lower:]')-javascript-port}" -bj_log "Using APP_DIR=$APP_REL_DIR mainName=$APP_MAIN_NAME package=$APP_PACKAGE_NAME" - -OUTPUT_ZIP="${1:-$HELLO_ROOT/parparvm/target/${APP_BUNDLE_BASENAME}.zip}" +OUTPUT_ZIP="${1:-$HELLO_ROOT/parparvm/target/hellocodenameone-javascript-port.zip}" TMPDIR="${TMPDIR:-/tmp}" TMPDIR="${TMPDIR%/}" @@ -85,7 +65,7 @@ if [ "${SKIP_MAVEN_BUILD:-0}" != "1" ] && [ "${SKIP_PARPARVM_BUILD:-0}" != "1" ] fi if [ "${SKIP_MAVEN_BUILD:-0}" != "1" ] && [ "${SKIP_COMMON_BUILD:-0}" != "1" ]; then - bj_log "Building $APP_MAIN_NAME common module and compile-scope dependencies" + bj_log "Building HelloCodenameOne common module and compile-scope dependencies" mkdir -p "$HOME/.codenameone" if [ -f "$REPO_ROOT/maven/UpdateCodenameOne.jar" ]; then cp "$REPO_ROOT/maven/UpdateCodenameOne.jar" "$HOME/.codenameone/" 2>/dev/null || true @@ -113,11 +93,10 @@ done STAGE_CLASSES="$WORK_DIR/stage-classes" PORT_CLASSES="$WORK_DIR/port-classes" SOURCE_LIST="$WORK_DIR/javascript-port-sources.txt" -LAUNCHER_CLASS_NAME="${APP_MAIN_NAME}JavaScriptMain" -LAUNCHER_SRC="$WORK_DIR/${LAUNCHER_CLASS_NAME}.java" +LAUNCHER_SRC="$WORK_DIR/HelloCodenameOneJavaScriptMain.java" TRANSLATOR_OUT="$WORK_DIR/translator-output" -TRANSLATOR_APP_NAME="$LAUNCHER_CLASS_NAME" -DIST_APP_NAME="$APP_MAIN_NAME" +TRANSLATOR_APP_NAME="HelloCodenameOneJavaScriptMain" +DIST_APP_NAME="HelloCodenameOne" mkdir -p "$STAGE_CLASSES" "$PORT_CLASSES" "$TRANSLATOR_OUT" bj_log "Staging JavaAPI and application classes" @@ -169,24 +148,24 @@ fi # Both launchers work - they bootstrap the implementation factory before Display.init() bj_log "Preparing JavaScript-port launcher" if [ "$TEAVM_AVAILABLE" -eq 1 ]; then - cat > "$LAUNCHER_SRC" < "$LAUNCHER_SRC" <<'EOF' import com.codename1.impl.html5.JavaScriptPortBootstrap; -import ${APP_PACKAGE_NAME}.${APP_MAIN_NAME}; +import com.codenameone.examples.hellocodenameone.HelloCodenameOne; -public final class ${LAUNCHER_CLASS_NAME} { +public final class HelloCodenameOneJavaScriptMain { public static void main(String[] args) { - JavaScriptPortBootstrap.bootstrap(new ${APP_MAIN_NAME}()); + JavaScriptPortBootstrap.bootstrap(new HelloCodenameOne()); } } EOF else - cat > "$LAUNCHER_SRC" < "$LAUNCHER_SRC" <<'EOF' import com.codename1.impl.html5.ParparVMBootstrap; -import ${APP_PACKAGE_NAME}.${APP_MAIN_NAME}; +import com.codenameone.examples.hellocodenameone.HelloCodenameOne; -public final class ${LAUNCHER_CLASS_NAME} { +public final class HelloCodenameOneJavaScriptMain { public static void main(String[] args) { - ParparVMBootstrap.bootstrap(new ${APP_MAIN_NAME}()); + ParparVMBootstrap.bootstrap(new HelloCodenameOne()); } } EOF @@ -245,14 +224,14 @@ bj_log "Compiling JavaScript-port runtime sources" "$JAVAC_BIN" -source 8 -target 8 -cp "$CLASSPATH" -d "$PORT_CLASSES" @"$SOURCE_LIST" cp -R "$PORT_CLASSES"/. "$STAGE_CLASSES"/ -bj_log "Running ByteCodeTranslator for $APP_MAIN_NAME" +bj_log "Running ByteCodeTranslator for HelloCodenameOne" "$JAVA_BIN" -cp "$PARPARVM_COMPILER" com.codename1.tools.translator.ByteCodeTranslator \ javascript \ "$STAGE_CLASSES" \ "$TRANSLATOR_OUT" \ "$TRANSLATOR_APP_NAME" \ - "$APP_PACKAGE_NAME" \ - "$APP_MAIN_NAME" \ + "com.codenameone.examples.hellocodenameone" \ + "HelloCodenameOne" \ "1.0" \ "ios" \ "none" diff --git a/scripts/input-validation-app/README.adoc b/scripts/input-validation-app/README.adoc index b225bff802..c27929fd81 100644 --- a/scripts/input-validation-app/README.adoc +++ b/scripts/input-validation-app/README.adoc @@ -2,8 +2,7 @@ A minimal Codename One app whose only purpose is to assert that physical input events (tap, drag, long-press) reach Component listeners end-to-end -on each port. Driven by per-platform OS-level automation (XCUITest on iOS, -Playwright on JavaScript). +on iOS. Driven by XCUITest on the iOS simulator. == Why a separate pipeline? @@ -24,8 +23,8 @@ emitted when each gesture fires. == Suite Each step waits up to 8 seconds for its expected event, then auto-advances -on success or timeout. The driver script (XCUITest or Playwright) is -responsible for issuing the actual OS input at the expected time. +on success or timeout. The XCUITest driver issues the actual OS input at +the expected time. [cols="1,3"] |=== @@ -49,7 +48,7 @@ responsible for issuing the actual OS input at the expected time. == Log markers -Everything the drivers care about is a single line on stdout: +Everything the driver cares about is a single line on stdout: ---- CN1IV:SUITE:STARTED platform= w= h= @@ -60,7 +59,7 @@ CN1IV:SUITE:FINISHED ---- A successful run contains one `READY:` + one `EVENT:` per step and no -`TIMEOUT:` lines. Drivers grep for these and fail non-zero on any miss. +`TIMEOUT:` lines. The driver greps for these and fails non-zero on any miss. == Layout @@ -69,82 +68,47 @@ scripts/input-validation-app/ ├── pom.xml # parent Maven project ├── common/ # CN1 Lifecycle + gesture screens ├── ios/ # CN1 iOS module (codename1.platform=ios) -├── javascript/ # CN1 JavaScript module (codename1.platform=javascript) -├── ios-tests/ # XCUITest target (XcodeGen-managed, no pbxproj) +├── ios-tests/ # XCUITest target (XcodeGen-managed) +│ ├── HostStub/ # minimal UIApplication host so the test +│ │ # runner has somewhere to attach -- the +│ │ # actual tests target the CN1 app by bundle id +│ └── Sources/ # XCUITest Swift code └── drivers/ - ├── run-ios.sh # boot sim, install, drive gestures, assert log - ├── run-js.sh # wrap Playwright driver - └── playwright-driver.mjs + └── run-ios.sh # boot sim, install, drive gestures, assert log ---- == Running locally -=== iOS - ---- -# 1. Build the CN1 iOS .app bundle (currently goes through the regular CN1 -# iOS build path; see TODO note in .github/workflows/input-validation.yml -# -- scripts/build-ios-app.sh is being generalised to accept this app). +# 1. Build the CN1 iOS .app bundle. cd scripts/input-validation-app mvn -P ios package APP_BUNDLE=$(find ios/target -name '*.app' -type d | head -n 1) -# 2. Drive the gesture suite on an iPhone 17 Pro simulator (override with -# CN1IV_DEVICE_NAME / CN1IV_DEVICE_RUNTIME). +# 2. Drive the gesture suite on whatever iPhone simulator is available +# (override with CN1IV_DEVICE_NAME / CN1IV_DEVICE_RUNTIME). brew install xcodegen # one-time ./drivers/run-ios.sh "$APP_BUNDLE" ---- -=== JavaScript +== CI ----- -# 1. Build the JS bundle and serve it (any static server will do): -cd scripts/input-validation-app -mvn -P javascript package -python3 -m http.server --directory javascript/target/ 8080 & +`.github/workflows/input-validation.yml` runs the iOS job on every PR. It +reuses the `_build-ios-port.yml` reusable workflow so it shares the same +port-build cache as the screenshot-based scripts-ios pipeline. -# 2. Drive gestures in headless Chromium: -npm install --no-save playwright -npx playwright install --with-deps chromium -./drivers/run-js.sh http://localhost:8080 ----- +The build script `scripts/build-ios-app.sh` accepts a +`CN1_APP_DIR=scripts/input-validation-app` override so the same script +can build either app; the default stays pointed at hellocodenameone so +the existing CI keeps passing. + +== Not yet covered (follow-up PRs) -== CI status - -Both jobs (iOS + JavaScript) run on every PR via -`.github/workflows/input-validation.yml`. The iOS job reuses the -`_build-ios-port.yml` reusable workflow so it shares the same port-build -cache as the screenshot-based scripts-ios pipeline; the JavaScript job -boots ParparVM + JavaScriptPort, bundles to a zip, serves it via -`python3 -m http.server`, and drives Chromium against `localhost:8080`. - -Both jobs are currently marked `continue-on-error: true` while the -input-dispatch ends of each port are stabilised: - -* **iOS**: XCUITest application-only mode (no host-app target) exits - with an opaque `** TEST FAILED **` under Xcode 16.4. The build / - install / launch / log-stream path is verified; the .xcresult - bundle is uploaded as an artifact for post-hoc inspection. -* **JavaScript**: synthetic DOM events reach main-thread listeners - (the driver's `layout:{...selfTestFired:true}` artifact confirms this) - but the CN1 JS port runs Java in a Web Worker and registers its - pointer listeners through a host-bridge proxy; synthetic events from - the test driver don't propagate through that proxy the way real OS - events do. This needs JS-port-side work before end-to-end input flow - can be asserted. - -The underlying build scripts (`scripts/build-ios-app.sh` and -`scripts/build-javascript-port-hellocodenameone.sh`) accept a -`CN1_APP_DIR=scripts/input-validation-app` override so the same scripts -build either app -- defaults stay pointed at hellocodenameone so the -existing CI keeps passing. - -== Not yet covered - -* Android (UIAutomator path + Linux emulator integration) -* JavaSE / desktop (java.awt.Robot) -* Status-bar tap-to-top -* Soft-keyboard show / character entry / dismiss -* Multi-touch / pinch - -These belong to follow-up PRs once this pipeline shape is validated. +* JavaScript port. Synthetic DOM events reach main-thread listeners but + the CN1 JS port registers its pointer listeners through a host-bridge + proxy from inside a Web Worker; synthetic events don't traverse that + proxy the way real OS events do. End-to-end input validation here + needs port-side changes that don't belong in this PR. +* Android (UIAutomator) on Linux. +* JavaSE / desktop (`java.awt.Robot`). +* Status-bar tap-to-top, soft-keyboard show/dismiss, multi-touch. diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java index b0801eb198..f52515a4f2 100644 --- a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/InputValidationApp.java @@ -5,8 +5,6 @@ package com.codenameone.inputvalidation; import com.codename1.system.Lifecycle; -import com.codename1.ui.CN; -import com.codename1.ui.Display; import com.codenameone.inputvalidation.gestures.GestureSuite; /// Lifecycle entry point for the input-validation CN1 app. The whole app does @@ -17,11 +15,6 @@ public class InputValidationApp extends Lifecycle { @Override public void runApp() { - Runnable suite = () -> new GestureSuite().start(); - if ("HTML5".equals(Display.getInstance().getPlatformName())) { - CN.callSerially(suite); - } else { - new Thread(suite, "CN1IV-Suite").start(); - } + new Thread(() -> new GestureSuite().start(), "CN1IV-Suite").start(); } } diff --git a/scripts/input-validation-app/drivers/playwright-driver.mjs b/scripts/input-validation-app/drivers/playwright-driver.mjs deleted file mode 100644 index cca9c96c57..0000000000 --- a/scripts/input-validation-app/drivers/playwright-driver.mjs +++ /dev/null @@ -1,236 +0,0 @@ -// Drive the CN1 input-validation app (JavaScript port) through tap / drag / -// long-press in headless Chromium and assert the expected CN1IV:EVENT lines -// appear on the browser console. Mirrors the iOS XCUITest driver -- same -// suite, same log markers, same pass/fail contract. - -import fs from 'node:fs'; -import path from 'node:path'; - -let chromium; -try { - ({ chromium } = await import('playwright')); -} catch (e1) { - try { - ({ chromium } = await import('@playwright/test')); - } catch (e2) { - console.error('Unable to load Playwright. Install "playwright" or "@playwright/test".'); - process.exit(2); - } -} - -const url = process.env.CN1IV_URL; -if (!url) { - console.error('CN1IV_URL env var is required (URL of the deployed JS build).'); - process.exit(2); -} - -const artifactsDir = process.env.CN1IV_ARTIFACTS_DIR - || path.resolve('artifacts/input-validation-js'); -fs.mkdirSync(artifactsDir, { recursive: true }); -const logPath = path.join(artifactsDir, 'browser.log'); -const logStream = fs.createWriteStream(logPath, { flags: 'w' }); - -const REQUIRED_EVENTS = [ - 'CN1IV:READY:tap', - 'CN1IV:EVENT:tap', - 'CN1IV:READY:drag', - 'CN1IV:EVENT:drag', - 'CN1IV:READY:longpress', - 'CN1IV:EVENT:longpress', - 'CN1IV:SUITE:FINISHED', -]; - -// We capture every console line, but only the lines containing CN1IV are -// considered for assertion. The page also dumps stack traces on errors, so -// we tee everything to disk for post-mortem. -const seen = new Set(); -const timeouts = []; -function record(line) { - logStream.write(line + '\n'); - if (line.indexOf('CN1IV:TIMEOUT:') >= 0) { - timeouts.push(line); - } - for (const m of REQUIRED_EVENTS) { - if (!seen.has(m) && line.indexOf(m) >= 0) { - seen.add(m); - } - } -} - -const browser = await chromium.launch({ headless: true }); -const viewport = { width: 393, height: 852 }; -try { - const page = await browser.newPage({ viewport, deviceScaleFactor: 2 }); - page.on('console', msg => record(`console:${msg.type()}:${msg.text()}`)); - page.on('pageerror', err => record(`pageerror:${String(err)}`)); - - await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 }); - - // Wait for the CN1 EDT to finish first paint before driving inputs. CN1's - // JS port emits CN1IV:READY:tap from GestureSuite.advance() right after - // Form.show(); we poll for it with a generous ceiling so flaky CI machines - // still pass. - const readyDeadline = Date.now() + 30_000; - while (Date.now() < readyDeadline && !seen.has('CN1IV:READY:tap')) { - await page.waitForTimeout(250); - } - if (!seen.has('CN1IV:READY:tap')) { - console.error('Timed out waiting for CN1IV:READY:tap from app.'); - process.exit(1); - } - - // Dump the DOM topology and verify the dispatch path before driving any - // gestures. The CN1 JS port registers pointer listeners on a - //
sibling of the canvas, so page-level - // clicks miss the listener. We dispatch synthetic events on the - // peersContainer directly. Pre-flight checks confirm dispatch is wired - // before we blame a CN1 regression for a missing event. - const layout = await page.evaluate(() => { - const canvas = document.querySelector('#codenameone-canvas'); - const peers = document.querySelector('#cn1-peers-container'); - const r = canvas ? canvas.getBoundingClientRect() : null; - const peersStyle = peers ? window.getComputedStyle(peers) : null; - let selfTestFired = false; - if (peers) { - const probe = (e) => { selfTestFired = true; }; - peers.addEventListener('mousedown', probe, true); - const evt = new MouseEvent('mousedown', { - bubbles: true, cancelable: true, view: window, - clientX: 10, clientY: 10, button: 0, buttons: 1, - }); - peers.dispatchEvent(evt); - peers.removeEventListener('mousedown', probe, true); - } - return { - hasCanvas: !!canvas, - hasPeersContainer: !!peers, - canvasRect: r ? { x: r.x, y: r.y, width: r.width, height: r.height } : null, - peersDisplay: peersStyle ? peersStyle.display : null, - peersPointerEvents: peersStyle ? peersStyle.pointerEvents : null, - peersPosition: peersStyle ? peersStyle.position : null, - selfTestFired, - }; - }); - record(`layout:${JSON.stringify(layout)}`); - if (!layout.hasPeersContainer) { - console.error('CN1 peers container not found; JS port did not initialise.'); - process.exit(1); - } - const rect = layout.canvasRect; - const px = (frac) => rect.x + rect.width * frac; - const py = (frac) => rect.y + rect.height * frac; - - // Dispatch mouse + pointer + touch variants for each gesture step. The CN1 - // JS port registers listeners on both mousedown/pointerdown and - // touchstart/touchend; firing all variants maximises the chance that one - // reaches the actual handler regardless of which path the port currently - // routes through. - async function fireMouse(type, x, y) { - await page.evaluate(({ type, x, y }) => { - const target = document.querySelector('#cn1-peers-container'); - const isUp = type === 'mouseup'; - const mouse = new MouseEvent(type, { - bubbles: true, cancelable: true, view: window, - clientX: x, clientY: y, button: 0, buttons: isUp ? 0 : 1, - }); - target.dispatchEvent(mouse); - const pointerType = type === 'mousedown' ? 'pointerdown' - : type === 'mouseup' ? 'pointerup' - : 'pointermove'; - try { - const ptr = new PointerEvent(pointerType, { - bubbles: true, cancelable: true, view: window, - clientX: x, clientY: y, button: 0, buttons: isUp ? 0 : 1, - pointerId: 1, pointerType: 'mouse', isPrimary: true, - }); - target.dispatchEvent(ptr); - } catch (_) {} - }, { type, x, y }); - } - - async function fireTouch(type, x, y) { - await page.evaluate(({ type, x, y }) => { - const target = document.querySelector('#cn1-peers-container'); - try { - const touch = new Touch({ - identifier: 1, target, clientX: x, clientY: y, - }); - const list = (type === 'touchend') ? [] : [touch]; - const evt = new TouchEvent(type, { - bubbles: true, cancelable: true, view: window, - touches: list, targetTouches: list, - changedTouches: [touch], - }); - target.dispatchEvent(evt); - } catch (_) {} - }, { type, x, y }); - } - - // Tap: down + up at the form centre. Touch first (matches real mobile), - // then mouse as belt-and-braces. - await fireTouch('touchstart', px(0.5), py(0.5)); - await fireMouse('mousedown', px(0.5), py(0.5)); - await page.waitForTimeout(80); - await fireTouch('touchend', px(0.5), py(0.5)); - await fireMouse('mouseup', px(0.5), py(0.5)); - await waitFor('CN1IV:EVENT:tap', 5_000); - - // Drag: down at left, several moves across, up at right. Need >= 3 - // pointerDragged samples for DragStep to fire. - await waitFor('CN1IV:READY:drag', 5_000); - await fireTouch('touchstart', px(0.2), py(0.55)); - await fireMouse('mousedown', px(0.2), py(0.55)); - for (let i = 1; i <= 10; i++) { - const frac = 0.2 + (0.6 * i / 10); - await fireTouch('touchmove', px(frac), py(0.55)); - await fireMouse('mousemove', px(frac), py(0.55)); - await page.waitForTimeout(20); - } - await fireTouch('touchend', px(0.8), py(0.55)); - await fireMouse('mouseup', px(0.8), py(0.55)); - await waitFor('CN1IV:EVENT:drag', 5_000); - - // Long-press: down, hold ~1.5s, up. CN1's threshold is ~1s. - await waitFor('CN1IV:READY:longpress', 5_000); - await fireTouch('touchstart', px(0.5), py(0.5)); - await fireMouse('mousedown', px(0.5), py(0.5)); - await page.waitForTimeout(1500); - await fireTouch('touchend', px(0.5), py(0.5)); - await fireMouse('mouseup', px(0.5), py(0.5)); - await waitFor('CN1IV:EVENT:longpress', 5_000); - - await waitFor('CN1IV:SUITE:FINISHED', 5_000); - - async function waitFor(marker, ms) { - const deadline = Date.now() + ms; - while (Date.now() < deadline) { - if (seen.has(marker)) return; - await page.waitForTimeout(150); - } - // Don't throw here -- continue so we get the full list of misses in the - // final report. - } -} finally { - await browser.close(); - logStream.end(); -} - -const log = (line) => process.stdout.write(`[playwright-driver] ${line}\n`); -let failed = false; -for (const m of REQUIRED_EVENTS) { - if (seen.has(m)) { - log(`OK ${m}`); - } else { - log(`MISS ${m}`); - failed = true; - } -} -if (timeouts.length) { - for (const t of timeouts) log(`TIMEOUT ${t}`); - failed = true; -} -if (failed) { - log(`Input-validation suite FAILED -- see ${logPath}`); - process.exit(1); -} -log('Input-validation suite PASSED'); diff --git a/scripts/input-validation-app/drivers/run-js.sh b/scripts/input-validation-app/drivers/run-js.sh deleted file mode 100755 index b83e909b89..0000000000 --- a/scripts/input-validation-app/drivers/run-js.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# Drive the CN1 input-validation app in headless Chromium against a URL -# serving the built JavaScript port. Thin wrapper around playwright-driver.mjs. -# -# Usage: -# run-js.sh -# -# Assumes Playwright (with Chromium) is installed -- the parent Maven build -# leaves a node_modules/ alongside the JS port output that has it; CI installs -# globally. -set -euo pipefail - -iv_log() { echo "[run-js] $1"; } - -if [ $# -lt 1 ]; then - iv_log "Usage: $0 " >&2 - exit 2 -fi - -URL="$1" - -if ! command -v node >/dev/null 2>&1; then - iv_log "node not on PATH" >&2 - exit 3 -fi - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -ARTIFACTS_DIR="${ARTIFACTS_DIR:-${GITHUB_WORKSPACE:-$APP_DIR}/artifacts/input-validation-js}" -mkdir -p "$ARTIFACTS_DIR" - -export CN1IV_URL="$URL" -export CN1IV_ARTIFACTS_DIR="$ARTIFACTS_DIR" - -iv_log "Driving $URL" -node "$SCRIPT_DIR/playwright-driver.mjs" diff --git a/scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift b/scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift new file mode 100644 index 0000000000..85bb62cbfd --- /dev/null +++ b/scripts/input-validation-app/ios-tests/HostStub/HostStubApp.swift @@ -0,0 +1,26 @@ +// Minimal UIKit host app for the XCUITest target. Application-only UI +// testing (no host app, USES_XCTRUNNER) errored with an opaque +// `** TEST FAILED **` under Xcode 16.4. Giving the test bundle a regular +// host -- even one that does nothing -- is the standard XCUITest setup and +// removes that failure mode. The actual UI tests attach to the +// already-installed CN1 input-validation app by bundle id via +// XCUIApplication(bundleIdentifier:), so what this stub does is irrelevant +// -- it just needs to be a launchable app the test runner can host into. + +import UIKit + +@UIApplicationMain +final class HostStubAppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + let w = UIWindow(frame: UIScreen.main.bounds) + let vc = UIViewController() + vc.view.backgroundColor = .black + w.rootViewController = vc + w.makeKeyAndVisible() + self.window = w + return true + } +} diff --git a/scripts/input-validation-app/ios-tests/project.yml b/scripts/input-validation-app/ios-tests/project.yml index c64d757b1f..e4bc533fa1 100644 --- a/scripts/input-validation-app/ios-tests/project.yml +++ b/scripts/input-validation-app/ios-tests/project.yml @@ -1,14 +1,18 @@ # XcodeGen project spec for the CN1 input-validation XCUITest target. # -# We deliberately do not check in a .xcodeproj -- pbxproj diffs are unreadable -# and break on every Xcode update. Instead the driver script invokes -# `xcodegen generate` against this file to materialise the project on demand. +# We don't check in a .xcodeproj -- pbxproj diffs are unreadable and break +# on every Xcode update. drivers/run-ios.sh runs `xcodegen generate` against +# this file to materialise the project on demand. # -# The XCUITest target launches the already-installed CN1 app by bundle id -# (see InputValidationUITests.swift) rather than building it here, so we have -# no host-app dependency and don't need provisioning profiles. The test runs -# under XCTest's `application-only` UI-testing mode, which works on simulators -# out of the box. +# Setup: +# - HostStub: a minimal UIApplication target the XCUITest bundle hosts +# into. Application-only UI testing (TEST_TARGET_NAME="") errors out +# opaquely under Xcode 16.4, so we give the test bundle a regular host. +# The stub does nothing -- the UI tests attach to the already-installed +# CN1 input-validation app via XCUIApplication(bundleIdentifier:). +# - CN1InputValidationUITests: the actual UI test bundle. TEST_HOST points +# at HostStub. Tests run by xcodebuild build the stub + the test bundle, +# install them, and then the Swift code launches the CN1 app by bundle id. name: CN1InputValidationUITests options: @@ -22,26 +26,41 @@ settings: base: SWIFT_VERSION: "5.0" CODE_SIGNING_ALLOWED: NO + CODE_SIGN_IDENTITY: "" targets: + HostStub: + type: application + platform: iOS + sources: + - HostStub + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.codenameone.cn1iv.hoststub + INFOPLIST_KEY_UILaunchStoryboardName: "" + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES + INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" + GENERATE_INFOPLIST_FILE: YES + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: YES + CN1InputValidationUITests: type: bundle.ui-testing platform: iOS sources: - Sources + dependencies: + - target: HostStub settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.codenameone.cn1iv.uitests - TEST_TARGET_NAME: "" - USES_XCTRUNNER: YES + TEST_TARGET_NAME: HostStub ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: YES - CODE_SIGNING_ALLOWED: NO - CODE_SIGN_IDENTITY: "" schemes: CN1InputValidationUITests: build: targets: + HostStub: [test] CN1InputValidationUITests: [test] test: targets: diff --git a/scripts/input-validation-app/javascript/pom.xml b/scripts/input-validation-app/javascript/pom.xml deleted file mode 100644 index 467b5e0b1f..0000000000 --- a/scripts/input-validation-app/javascript/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - 4.0.0 - - com.codenameone.examples.inputvalidation - cn1-input-validation - 1.0-SNAPSHOT - - com.codenameone.examples.inputvalidation - cn1-input-validation-javascript - 1.0-SNAPSHOT - - - UTF-8 - 17 - 17 - javascript - javascript - javascript - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.1 - - - default-jar - none - - - - - com.codenameone - codenameone-maven-plugin - ${cn1.plugin.version} - - - build-javascript - package - - build - - - - - - - - - - ${project.groupId} - ${cn1app.name}-common - ${project.version} - - - diff --git a/scripts/input-validation-app/pom.xml b/scripts/input-validation-app/pom.xml index 112ec8361a..b48c037d48 100644 --- a/scripts/input-validation-app/pom.xml +++ b/scripts/input-validation-app/pom.xml @@ -86,17 +86,5 @@ ios - - javascript - - - codename1.platform - javascript - - - - javascript - - From 241dbf0feb446eb2d9a458af0e97d7f86f76b275 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 20:40:45 +0300 Subject: [PATCH 8/8] Fix iOS XCUITest: correct bundle id, drop misleading env var The xcresult artifact from the previous run pinpointed the actual failure: "Failed to launch com.codenameone.examples.inputvalidation". The Swift fallback had `examples.` baked in but the CN1 maven plugin derives CFBundleIdentifier straight from `codename1.packageName`, which is `com.codenameone.inputvalidation` (no `examples.`) per common/codenameone_settings.properties. The CN1IV_BUNDLE_ID env override was also broken: `xcodebuild test KEY=VALUE` sets a *build setting*, not a runtime environment variable for the test process. The Swift code's ProcessInfo lookup would never see it, so the fallback was always used. Hard-code the correct id; drop the broken env var passing; warn from run-ios.sh if the installed bundle id doesn't match what the test will request. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../input-validation-app/drivers/run-ios.sh | 21 +++++++++++++------ .../Sources/InputValidationUITests.swift | 11 ++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/scripts/input-validation-app/drivers/run-ios.sh b/scripts/input-validation-app/drivers/run-ios.sh index ca97108023..22a0ec14af 100755 --- a/scripts/input-validation-app/drivers/run-ios.sh +++ b/scripts/input-validation-app/drivers/run-ios.sh @@ -133,11 +133,21 @@ trap cleanup EXIT INT TERM iv_log "Generating XCUITest project via xcodegen" ( cd "$TESTS_DIR" && xcodegen generate >> "$XCODEBUILD_LOG" 2>&1 ) -# Run the XCUITest suite. CN1IV_BUNDLE_ID tells the Swift code which app to -# attach to; CN1IV_STEP_DELAY_SEC lets us slow the inter-gesture wait on -# heavily loaded CI runners. `-resultBundlePath` captures the .xcresult so -# we can extract the actual test failure reason post-hoc (without it, -# xcodebuild just prints `** TEST FAILED **`). +# Run the XCUITest suite. The Swift code uses XCUIApplication(bundleIdentifier:) +# with a hard-coded id that mirrors common/codenameone_settings.properties -- +# xcodebuild `KEY=VALUE` args are build settings, not runtime env vars, so we +# can't pass the bundle id through here. The driver verifies the installed +# bundle id matches what the test will request before launching xcodebuild, +# rather than trying to thread it into the test process. +EXPECTED_BUNDLE_ID="com.codenameone.inputvalidation" +if [ "$BUNDLE_ID" != "$EXPECTED_BUNDLE_ID" ]; then + iv_log "WARNING: installed bundle id ($BUNDLE_ID) does not match the value" + iv_log "WARNING: hard-coded in InputValidationUITests.swift ($EXPECTED_BUNDLE_ID)." + iv_log "WARNING: Update both or XCUITest will fail to launch the app." +fi +# `-resultBundlePath` captures the .xcresult so we can extract the actual +# test failure reason post-hoc (without it, xcodebuild just prints +# `** TEST FAILED **`). XCRESULT_BUNDLE="$ARTIFACTS_DIR/test.xcresult" rm -rf "$XCRESULT_BUNDLE" iv_log "Running XCUITest" @@ -147,7 +157,6 @@ xcodebuild test \ -scheme CN1InputValidationUITests \ -destination "platform=iOS Simulator,id=$SIM_UDID" \ -resultBundlePath "$XCRESULT_BUNDLE" \ - CN1IV_BUNDLE_ID="$BUNDLE_ID" \ CODE_SIGNING_ALLOWED=NO \ | tee -a "$XCODEBUILD_LOG" XCB_RC=${PIPESTATUS[0]} diff --git a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift index cb1ab2c712..725871a2a4 100644 --- a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift +++ b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift @@ -12,12 +12,15 @@ import XCTest final class InputValidationUITests: XCTestCase { - // Bundle identifier of the CN1-built iOS app under test. Set from the - // CN1IV_BUNDLE_ID env var so the driver script can target whatever value - // the CN1 Maven plugin generated, without us hard-coding it. + // Bundle identifier of the CN1-built iOS app under test. The CN1 maven + // plugin derives the iOS CFBundleIdentifier from + // `codename1.packageName` in common/codenameone_settings.properties, so + // keeping that property and this default in sync is enough. The + // CN1IV_BUNDLE_ID env var override is for local runs against an app + // built with a different packageName. private var bundleIdentifier: String { return ProcessInfo.processInfo.environment["CN1IV_BUNDLE_ID"] - ?? "com.codenameone.examples.inputvalidation" + ?? "com.codenameone.inputvalidation" } private var stepDelaySeconds: TimeInterval {