gowalk-cicd 1.0.134 → 1.0.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -0
- package/README.md +4 -0
- package/action/.daemux-version +1 -1
- package/action/scripts/native_bundle.py +22 -2
- package/action/scripts/native_pods_retry.py +16 -1
- package/action/scripts/native_prepare_evidence.py +6 -0
- package/action/scripts/test_native_pods_retry.py +47 -0
- package/action/scripts/test_native_prepare_evidence.py +26 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/jvm_proxy_relay.py +3 -2
- package/android-action/scripts/test_jvm_proxy_loopback.py +91 -0
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -142,6 +142,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
142
142
|
and HTTPS_PROXY variables also name the credential-free loopback relay because
|
|
143
143
|
Crashlytics Buildtools reads them first and rejects authenticated proxy URLs. HTTPS is tunneled
|
|
144
144
|
without TLS interception; no upstream failure may fall back to the destination.
|
|
145
|
+
Java bypasses the relay only for exact `localhost`, `127.0.0.1` and `[::1]`
|
|
146
|
+
targets so owned HTTP test fixtures remain local. Never widen that host list;
|
|
147
|
+
external requests and environment-first clients retain the assigned relay.
|
|
145
148
|
The wrapper disables Gradle daemons and closes its relay when the command ends.
|
|
146
149
|
The relay's idle deadline covers the whole tunnel: traffic in either direction
|
|
147
150
|
keeps downloads/uploads alive. An idle direction alone must never end a transfer.
|
package/README.md
CHANGED
|
@@ -517,6 +517,10 @@ The relay authenticates to that exit without putting credentials in JVM options;
|
|
|
517
517
|
HTTPS remains an end-to-end TLS tunnel. An unavailable proxy fails the request,
|
|
518
518
|
and the relay shuts down with its child command. Gradle daemons are disabled for
|
|
519
519
|
these commands so they cannot retain an expired relay port.
|
|
520
|
+
Java requests to exact `localhost`, `127.0.0.1` and `[::1]` targets stay local for
|
|
521
|
+
HTTP test fixtures. All other Java hosts, including lookalike names such as
|
|
522
|
+
`localhost.example.com`, retain the assigned proxy; environment-first clients
|
|
523
|
+
continue to use the relay.
|
|
520
524
|
|
|
521
525
|
### Private git dependencies (Flutter)
|
|
522
526
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.136
|
|
@@ -18,6 +18,27 @@ import native_pods_process
|
|
|
18
18
|
from native_prepare_evidence import report
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
# The Ruby bundle is only one of the things this script can refuse over, but every
|
|
22
|
+
# refusal used to carry the lockfile remediation below. printer-ai run 34374868740
|
|
23
|
+
# failed because CocoaPods could not clone a dependency from github.com, and the
|
|
24
|
+
# receipt told the session to "reconcile dependency locks locally" — locks that were
|
|
25
|
+
# never wrong. A configuration command that fails inside its own child publishes its
|
|
26
|
+
# own receipt and signals; point the reader at those instead (2026-09-09).
|
|
27
|
+
BUNDLE_REMEDIATION = ("Use the app's committed Gemfile and Gemfile.lock with a compatible Ruby/Bundler "
|
|
28
|
+
"runtime; reconcile dependency locks locally and retain deployment guards.")
|
|
29
|
+
COMMAND_REMEDIATION = ("The configuration command failed inside its own child, not in the Ruby bundle. "
|
|
30
|
+
"Read that child's own receipt and native_prepare_failed signals for the cause, "
|
|
31
|
+
"and repair what they name.")
|
|
32
|
+
BUNDLE_CODES = frozenset({
|
|
33
|
+
"native_gemfile_required", "native_gemfile_commit_required", "native_gemfile_lock_required",
|
|
34
|
+
"native_gemfile_lock_commit_required", "native_bundle_install_failed", "native_bundle_inputs_changed",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def remediation(code: str) -> str:
|
|
39
|
+
return BUNDLE_REMEDIATION if code in BUNDLE_CODES else COMMAND_REMEDIATION
|
|
40
|
+
|
|
41
|
+
|
|
21
42
|
def command(argv: list[str], cwd: Path, log, *, timeout: float = 900) -> int:
|
|
22
43
|
try:
|
|
23
44
|
return native_pods_process.command(argv, cwd, log, timeout=timeout)
|
|
@@ -101,8 +122,7 @@ def main(argv: list[str]) -> int:
|
|
|
101
122
|
answer = run(args.root, args.folder, args.proxy_helper, command_args)
|
|
102
123
|
except (guard.Refused, OSError, subprocess.SubprocessError) as exc:
|
|
103
124
|
code = str(exc) if isinstance(exc, guard.Refused) else "native_bundle_operation_unverified"
|
|
104
|
-
answer = {"ok": False, "code": code, "next":
|
|
105
|
-
"compatible Ruby/Bundler runtime; reconcile dependency locks locally and retain deployment guards."}
|
|
125
|
+
answer = {"ok": False, "code": code, "next": remediation(code)}
|
|
106
126
|
if getattr(exc, "diagnostic", ""):
|
|
107
127
|
answer["diagnostic"] = exc.diagnostic
|
|
108
128
|
report(answer, "bundle")
|
|
@@ -19,6 +19,15 @@ PARTIAL = re.compile(r"\bcurl(?:\s+18\b|:\s*\(18\))", re.I)
|
|
|
19
19
|
# downloaded: <url> Response: Timeout was reached". A timeout leaves the successfully
|
|
20
20
|
# fetched specs cached, so the second attempt resumes rather than starting over.
|
|
21
21
|
TIMED_OUT = re.compile(r"Response:\s*Timeout was reached|\bcurl(?:\s+28\b|:\s*\(28\))", re.I)
|
|
22
|
+
# A handshake that dies mid-negotiation names no curl code and no verdict about the
|
|
23
|
+
# peer, so neither pattern above can see it: printer-ai run 34374868740 lost the iOS
|
|
24
|
+
# build to `LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443`
|
|
25
|
+
# while CocoaPods cloned a dependency, and 2phone-ios run 34374984763 lost the same
|
|
26
|
+
# step to bundler's `SSL_connect returned=1 ... unexpected eof while reading`
|
|
27
|
+
# (2026-09-09). Both are a connection the exit dropped before either side spoke, which
|
|
28
|
+
# the next attempt usually completes. A CERTIFICATE verdict is a different fact and
|
|
29
|
+
# stays permanent above: TERMINAL still vetoes this class whenever it appears.
|
|
30
|
+
HANDSHAKE = re.compile(r"SSL_ERROR_SYSCALL|unexpected eof while reading", re.I)
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
def interrupted_transfer(body: str) -> bool:
|
|
@@ -30,11 +39,17 @@ def timed_out(body: str) -> bool:
|
|
|
30
39
|
return bool(TIMED_OUT.search(body) and not TERMINAL.search(body))
|
|
31
40
|
|
|
32
41
|
|
|
42
|
+
def handshake_reset(body: str) -> bool:
|
|
43
|
+
return bool(HANDSHAKE.search(body) and not TERMINAL.search(body))
|
|
44
|
+
|
|
45
|
+
|
|
33
46
|
def retryable(body: str) -> str:
|
|
34
47
|
"""Name the transient class this failure belongs to, or "" when it is not one."""
|
|
35
48
|
if interrupted_transfer(body):
|
|
36
49
|
return "partial_transfer"
|
|
37
|
-
|
|
50
|
+
if timed_out(body):
|
|
51
|
+
return "transient_timeout"
|
|
52
|
+
return "handshake_reset" if handshake_reset(body) else ""
|
|
38
53
|
|
|
39
54
|
|
|
40
55
|
def install(argv: list[str], folder, log, snapshot: dict, *, timeout: float = 900) -> tuple[int, int]:
|
|
@@ -10,6 +10,12 @@ SIGNALS = {
|
|
|
10
10
|
"bundler_version_missing": ("Could not find 'bundler'",),
|
|
11
11
|
"tls_verification": ("certificate verify failed", "CERTIFICATE_VERIFY_FAILED"),
|
|
12
12
|
"connection_timeout": ("Connection timed out", "execution expired", "Read timed out"),
|
|
13
|
+
# Distinct from tls_verification above: the peer never returned a verdict about its
|
|
14
|
+
# certificate, the connection died mid-handshake. printer-ai run 34374868740
|
|
15
|
+
# published signals=[] for `SSL_ERROR_SYSCALL in connection to github.com:443`
|
|
16
|
+
# (2026-09-09), so the session read an unclassified failure and repaired the wrong
|
|
17
|
+
# thing. Neither token appears in a certificate verdict.
|
|
18
|
+
"tls_handshake_reset": ("SSL_ERROR_SYSCALL", "unexpected eof while reading"),
|
|
13
19
|
"dns_resolution": ("Could not resolve host", "getaddrinfo"),
|
|
14
20
|
"partial_transfer": ("curl 18", "curl: (18)"),
|
|
15
21
|
"disk_full": ("No space left on device",),
|
|
@@ -16,6 +16,16 @@ PARTIAL = "error: RPC failed; curl 18 transfer closed with outstanding read data
|
|
|
16
16
|
# redaction — the form `retryable` actually sees.
|
|
17
17
|
TIMEOUT = "\n".join(["CDN: trunk URL couldn't be downloaded: [URL redacted] "
|
|
18
18
|
"Response: Timeout was reached"] * 30)
|
|
19
|
+
# Verbatim from printer-ai Mobile Deploy 34374868740, after the diagnostic's own URL
|
|
20
|
+
# redaction: CocoaPods cloning a dependency when the exit dropped the connection.
|
|
21
|
+
HANDSHAKE = ("Cloning into '[long value redacted]'...\n"
|
|
22
|
+
"fatal: unable to access '[URL redacted]': LibreSSL SSL_connect: "
|
|
23
|
+
"SSL_ERROR_SYSCALL in connection to github.com:443")
|
|
24
|
+
# The same fault one layer earlier, from 2phone-ios run 34374984763: bundler fetching
|
|
25
|
+
# specs. It names no curl code and no SSL_ERROR_SYSCALL, only the truncated read.
|
|
26
|
+
BUNDLER_EOF = ("Retrying fetcher due to error (2/2): Bundler::HTTPError Could not fetch specs "
|
|
27
|
+
"due to underlying error <SSL_connect returned=1 errno=0 state=error: "
|
|
28
|
+
"unexpected eof while reading>")
|
|
19
29
|
|
|
20
30
|
|
|
21
31
|
class RetryTests(unittest.TestCase):
|
|
@@ -121,6 +131,43 @@ class RetryTests(unittest.TestCase):
|
|
|
121
131
|
with self.subTest(body=body):
|
|
122
132
|
self.assertEqual(native_pods_retry.retryable(body), "")
|
|
123
133
|
|
|
134
|
+
def test_dropped_handshake_recovers_within_the_same_deadline(self):
|
|
135
|
+
for failure in (HANDSHAKE, BUNDLER_EOF):
|
|
136
|
+
with self.subTest(failure=failure.splitlines()[-1][:40]):
|
|
137
|
+
(self.root / "attempts.json").unlink(missing_ok=True)
|
|
138
|
+
self.script([failure])
|
|
139
|
+
code, result = self.fixture.run_cli()
|
|
140
|
+
self.assertEqual((code, result["pod_attempts"]), (0, 2))
|
|
141
|
+
attempts = json.loads((self.root / "attempts.json").read_text())
|
|
142
|
+
self.assertEqual(len(attempts), 2)
|
|
143
|
+
for attempt in attempts:
|
|
144
|
+
self.assertEqual(attempt["argv"], ["install", "--deployment", "--no-repo-update"])
|
|
145
|
+
self.assertTrue(attempt["proxy"].startswith("http://127.0.0.1:"))
|
|
146
|
+
self.assertEqual((self.root / "Podfile.lock").read_text(), "fixture-lock")
|
|
147
|
+
|
|
148
|
+
def test_repeat_dropped_handshake_stops_after_second_attempt(self):
|
|
149
|
+
self.script([HANDSHAKE, HANDSHAKE, HANDSHAKE])
|
|
150
|
+
code, result = self.fixture.run_cli()
|
|
151
|
+
self.assertEqual((code, result["code"]), (1, "native_pods_install_failed"))
|
|
152
|
+
self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 2)
|
|
153
|
+
|
|
154
|
+
def test_a_certificate_verdict_beside_a_dropped_handshake_stays_permanent(self):
|
|
155
|
+
for failure in ("certificate verify failed", "SSL peer shut down incorrectly",
|
|
156
|
+
"The requested URL returned error: 403", "curl: (60) unknown trust failure"):
|
|
157
|
+
with self.subTest(failure=failure):
|
|
158
|
+
(self.root / "attempts.json").unlink(missing_ok=True)
|
|
159
|
+
self.script([HANDSHAKE + "\n" + failure])
|
|
160
|
+
self.assertEqual(self.fixture.run_cli()[0], 1)
|
|
161
|
+
self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 1)
|
|
162
|
+
|
|
163
|
+
def test_a_dropped_handshake_is_its_own_named_class(self):
|
|
164
|
+
self.assertEqual(native_pods_retry.retryable(HANDSHAKE), "handshake_reset")
|
|
165
|
+
self.assertEqual(native_pods_retry.retryable(BUNDLER_EOF), "handshake_reset")
|
|
166
|
+
for body in ("the peer read the file", "eof while reading the manifest",
|
|
167
|
+
"connection established", "SSL is configured"):
|
|
168
|
+
with self.subTest(body=body):
|
|
169
|
+
self.assertEqual(native_pods_retry.retryable(body), "")
|
|
170
|
+
|
|
124
171
|
def test_one_deadline_and_no_guess_from_generic_eof(self):
|
|
125
172
|
for body in ("fatal: early EOF", "curl 180 failure", "unknown network error"):
|
|
126
173
|
self.assertFalse(native_pods_retry.interrupted_transfer(body))
|
|
@@ -30,6 +30,32 @@ class EvidenceTests(unittest.TestCase):
|
|
|
30
30
|
self.assertEqual(receipt["phase"], phase)
|
|
31
31
|
self.assertEqual(receipt["signals"], ["pod_version_conflict"])
|
|
32
32
|
|
|
33
|
+
def test_a_dropped_handshake_is_named_and_is_not_a_certificate_verdict(self):
|
|
34
|
+
for diagnostic, expected in [
|
|
35
|
+
("fatal: unable to access '[URL redacted]': LibreSSL SSL_connect: "
|
|
36
|
+
"SSL_ERROR_SYSCALL in connection to github.com:443", ["tls_handshake_reset"]),
|
|
37
|
+
("Bundler::HTTPError Could not fetch specs due to underlying error "
|
|
38
|
+
"<SSL_connect returned=1 errno=0 state=error: unexpected eof while reading>",
|
|
39
|
+
["tls_handshake_reset"]),
|
|
40
|
+
("SSL_connect returned=1 errno=0 state=error: certificate verify failed",
|
|
41
|
+
["tls_verification"]),
|
|
42
|
+
]:
|
|
43
|
+
err = io.StringIO()
|
|
44
|
+
with self.subTest(diagnostic=diagnostic[:40]), contextlib.redirect_stderr(err):
|
|
45
|
+
report({"ok": False, "code": "native_pods_install_failed", "diagnostic": diagnostic}, "pods")
|
|
46
|
+
self.assertEqual(json.loads(err.getvalue().strip().split("::", 2)[2])["signals"], expected)
|
|
47
|
+
|
|
48
|
+
def test_a_failed_child_command_is_not_reported_as_a_ruby_bundle_fault(self):
|
|
49
|
+
self.assertEqual(native_bundle.remediation("native_gemfile_lock_commit_required"),
|
|
50
|
+
native_bundle.BUNDLE_REMEDIATION)
|
|
51
|
+
self.assertEqual(native_bundle.remediation("native_bundle_install_failed"),
|
|
52
|
+
native_bundle.BUNDLE_REMEDIATION)
|
|
53
|
+
for code in ("native_configuration_failed", "native_pods_operation_interrupted",
|
|
54
|
+
"native_bundle_operation_unverified", "native_google_proxy_required"):
|
|
55
|
+
with self.subTest(code=code):
|
|
56
|
+
self.assertEqual(native_bundle.remediation(code), native_bundle.COMMAND_REMEDIATION)
|
|
57
|
+
self.assertNotIn("Gemfile", native_bundle.remediation(code))
|
|
58
|
+
|
|
33
59
|
def test_success_is_silent_and_unknown_failures_cannot_inject_annotations(self):
|
|
34
60
|
err = io.StringIO()
|
|
35
61
|
with contextlib.redirect_stderr(err):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.136
|
|
@@ -149,7 +149,7 @@ def running(proxy: str):
|
|
|
149
149
|
|
|
150
150
|
def environment(env: dict, port: int) -> dict:
|
|
151
151
|
# Crashlytics Buildtools prefers HTTP_PROXY to JVM options and rejects URLs
|
|
152
|
-
# with userinfo.
|
|
152
|
+
# with userinfo. External child requests use the same owned relay; authentication
|
|
153
153
|
# is added only on its connection to the configured account exit.
|
|
154
154
|
local = f"http://127.0.0.1:{port}"
|
|
155
155
|
for name in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
|
|
@@ -157,7 +157,8 @@ def environment(env: dict, port: int) -> dict:
|
|
|
157
157
|
env.update(NO_PROXY="", no_proxy="")
|
|
158
158
|
options = (f"-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort={port} "
|
|
159
159
|
f"-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort={port} "
|
|
160
|
-
"-Dhttp.nonProxyHosts=
|
|
160
|
+
"-Dhttp.nonProxyHosts=localhost|127.0.0.1|[::1] "
|
|
161
|
+
"-Dhttps.nonProxyHosts=localhost|127.0.0.1|[::1] -Djava.net.useSystemProxies=false")
|
|
161
162
|
# Java prints these options at startup; they contain no proxy credentials.
|
|
162
163
|
for name in ("JAVA_TOOL_OPTIONS", "JDK_JAVA_OPTIONS", "_JAVA_OPTIONS", "GRADLE_OPTS"):
|
|
163
164
|
env[name] = (env.get(name, "") + " " + options).strip()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Local HTTP fixtures stay local while every external Java request keeps the assigned relay."""
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
import jvm_proxy_relay as relay
|
|
14
|
+
from test_jvm_proxy_relay import upstream
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def fixture(ipv6=False):
|
|
19
|
+
seen = []
|
|
20
|
+
|
|
21
|
+
class Handler(BaseHTTPRequestHandler):
|
|
22
|
+
def do_GET(self):
|
|
23
|
+
seen.append(self.path)
|
|
24
|
+
self.send_response(200)
|
|
25
|
+
self.end_headers()
|
|
26
|
+
self.wfile.write(b"owned local fixture")
|
|
27
|
+
|
|
28
|
+
def log_message(self, *_):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
class Server(ThreadingHTTPServer):
|
|
32
|
+
address_family = socket.AF_INET6 if ipv6 else socket.AF_INET
|
|
33
|
+
|
|
34
|
+
server = Server(("::1" if ipv6 else "127.0.0.1", 0), Handler)
|
|
35
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
36
|
+
thread.start()
|
|
37
|
+
try:
|
|
38
|
+
yield server.server_port, seen
|
|
39
|
+
finally:
|
|
40
|
+
server.shutdown()
|
|
41
|
+
server.server_close()
|
|
42
|
+
thread.join(timeout=5)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def java_probe(port, urls):
|
|
46
|
+
source = '''import java.net.*;
|
|
47
|
+
public class Probe {
|
|
48
|
+
public static void main(String[] args) throws Exception {
|
|
49
|
+
for (var url : args) {
|
|
50
|
+
var connection = URI.create(url).toURL().openConnection();
|
|
51
|
+
connection.setConnectTimeout(3000); connection.setReadTimeout(3000);
|
|
52
|
+
System.out.println(new String(connection.getInputStream().readAllBytes()));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}'''
|
|
56
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
57
|
+
path = Path(directory) / "Probe.java"
|
|
58
|
+
path.write_text(source)
|
|
59
|
+
env = relay.environment({**os.environ, "JAVA_TOOL_OPTIONS": "-Dhttp.nonProxyHosts=*"}, port)
|
|
60
|
+
return subprocess.run(["java", str(path), *urls], env=env, capture_output=True, text=True, timeout=20)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@unittest.skipUnless(shutil.which("java"), "Java runtime unavailable")
|
|
64
|
+
class LoopbackTests(unittest.TestCase):
|
|
65
|
+
def test_ipv4_and_localhost_fixtures_bypass_only_the_owned_relay(self):
|
|
66
|
+
with fixture() as (local, requests), upstream() as (proxy, seen), relay.running(proxy) as port:
|
|
67
|
+
result = java_probe(port, [f"http://127.0.0.1:{local}/literal", f"http://localhost:{local}/hostname",
|
|
68
|
+
"http://unresolvable.invalid/provider", "http://localhost.invalid/provider"])
|
|
69
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
70
|
+
self.assertEqual(result.stdout.splitlines(), ["owned local fixture", "owned local fixture",
|
|
71
|
+
"through pinned proxy", "through pinned proxy"])
|
|
72
|
+
self.assertEqual(requests, ["/literal", "/hostname"])
|
|
73
|
+
self.assertEqual([row[1] for row in seen], ["http://unresolvable.invalid/provider",
|
|
74
|
+
"http://localhost.invalid/provider"])
|
|
75
|
+
self.assertTrue(all(row[2].startswith("Basic ") for row in seen))
|
|
76
|
+
self.assertNotIn("fixture-pass", result.stderr)
|
|
77
|
+
|
|
78
|
+
def test_ipv6_loopback_fixture_remains_local(self):
|
|
79
|
+
try:
|
|
80
|
+
with fixture(ipv6=True) as (local, requests), upstream() as (proxy, seen), relay.running(proxy) as port:
|
|
81
|
+
result = java_probe(port, [f"http://[::1]:{local}/ipv6", "http://unresolvable.invalid/provider"])
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
self.skipTest(f"IPv6 fixture unavailable: {exc.errno}")
|
|
84
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
85
|
+
self.assertEqual(result.stdout.splitlines(), ["owned local fixture", "through pinned proxy"])
|
|
86
|
+
self.assertEqual(requests, ["/ipv6"])
|
|
87
|
+
self.assertEqual([row[1] for row in seen], ["http://unresolvable.invalid/provider"])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.136
|