diff options
Diffstat (limited to 'tests/untestable')
-rw-r--r-- | tests/untestable/gdb/gdb_pretty_printer_test.py | 64 | ||||
-rw-r--r-- | tests/untestable/gdb/gdb_pretty_printer_test_program.nim | 89 | ||||
-rwxr-xr-x | tests/untestable/gdb/gdb_pretty_printer_test_run.sh | 13 | ||||
-rw-r--r-- | tests/untestable/network/README.md | 8 | ||||
-rw-r--r-- | tests/untestable/network/stdlib/tnet.nim | 16 | ||||
-rw-r--r-- | tests/untestable/readme.markdown | 9 | ||||
-rw-r--r-- | tests/untestable/thttpclient_ssl_disabled.nim | 36 | ||||
-rw-r--r-- | tests/untestable/thttpclient_ssl_env_var.nim | 74 | ||||
-rw-r--r-- | tests/untestable/thttpclient_ssl_remotenetwork.nim | 230 | ||||
-rw-r--r-- | tests/untestable/tpostgres.nim | 1 | ||||
-rw-r--r-- | tests/untestable/tssl.nim | 36 |
11 files changed, 576 insertions, 0 deletions
diff --git a/tests/untestable/gdb/gdb_pretty_printer_test.py b/tests/untestable/gdb/gdb_pretty_printer_test.py new file mode 100644 index 000000000..aed0cfeb0 --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test.py @@ -0,0 +1,64 @@ +import gdb +import re +import sys +# this test should test the gdb pretty printers of the nim +# library. But be aware this test is not complete. It only tests the +# command line version of gdb. It does not test anything for the +# machine interface of gdb. This means if if this test passes gdb +# frontends might still be broken. + +gdb.execute("set python print-stack full") +gdb.execute("source ../../../tools/debug/nim-gdb.py") +# debug all instances of the generic function `myDebug`, should be 14 +gdb.execute("rbreak myDebug") +gdb.execute("run") + +outputs = [ + 'meTwo', + '""', + '"meTwo"', + '{meOne, meThree}', + 'MyOtherEnum(1)', + '{MyOtherEnum(0), MyOtherEnum(2)}', + 'array = {1, 2, 3, 4, 5}', + 'seq(0, 0)', + 'seq(0, 10)', + 'array = {"one", "two"}', + 'seq(3, 3) = {1, 2, 3}', + 'seq(3, 3) = {"one", "two", "three"}', + 'Table(3, 64) = {[4] = "four", [5] = "five", [6] = "six"}', + 'Table(3, 8) = {["two"] = 2, ["three"] = 3, ["one"] = 1}', + '{a = 1, b = "some string"}', + '("hello", 42)' +] + +argRegex = re.compile("^.* = (?:No suitable Nim \$ operator found for type: \w+\s*)*(.*)$") +# Remove this error message which can pop up +noSuitableRegex = re.compile("(No suitable Nim \$ operator found for type: \w+\s*)") + +for i, expected in enumerate(outputs): + gdb.write(f"\x1b[38;5;105m{i+1}) expecting: {expected}: \x1b[0m", gdb.STDLOG) + gdb.flush() + currFrame = gdb.selected_frame() + functionSymbol = currFrame.block().function + assert functionSymbol.line == 24, str(functionSymbol.line) + raw = "" + if i == 6: + # myArray is passed as pointer to int to myDebug. I look up myArray up in the stack + gdb.execute("up") + raw = gdb.parse_and_eval("myArray") + elif i == 9: + # myOtherArray is passed as pointer to int to myDebug. I look up myOtherArray up in the stack + gdb.execute("up") + raw = gdb.parse_and_eval("myOtherArray") + else: + rawArg = re.sub(noSuitableRegex, "", gdb.execute("info args", to_string = True)) + raw = rawArg.split("=", 1)[-1].strip() + output = str(raw) + + if output != expected: + gdb.write(f"\x1b[38;5;196m ({output}) != expected: ({expected})\x1b[0m\n", gdb.STDERR) + gdb.execute("quit 1") + else: + gdb.write("\x1b[38;5;34mpassed\x1b[0m\n", gdb.STDLOG) + gdb.execute("continue") diff --git a/tests/untestable/gdb/gdb_pretty_printer_test_program.nim b/tests/untestable/gdb/gdb_pretty_printer_test_program.nim new file mode 100644 index 000000000..163c99860 --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test_program.nim @@ -0,0 +1,89 @@ + + +import tables + +type + MyEnum = enum + meOne, + meTwo, + meThree, + meFour, + + MyOtherEnum = enum + moOne, + moTwo, + moThree, + moFoure, + + MyObj = object + a*: int + b*: string + +var counter = 0 + +proc myDebug[T](arg: T): void = + counter += 1 + +proc testProc(): void = + var myEnum = meTwo + myDebug(myEnum) #1 + + # create a string, but don't allocate it + var myString: string + myDebug(myString) #2 + + # create a string object but also make the NTI for MyEnum is generated + myString = $myEnum + myDebug(myString) #3 + + var mySet = {meOne,meThree} + myDebug(mySet) #4 + + # for MyOtherEnum there is no NTI. This tests the fallback for the pretty printer. + var moEnum = moTwo + myDebug(moEnum) #5 + + var moSet = {moOne,moThree} + myDebug(moSet) #6 + + let myArray = [1,2,3,4,5] + myDebug(myArray) #7 + + # implicitly initialized seq test + var mySeq: seq[string] + myDebug(mySeq) #8 + + # len not equal to capacity + let myOtherSeq = newSeqOfCap[string](10) + myDebug(myOtherSeq) #9 + + let myOtherArray = ["one","two"] + myDebug(myOtherArray) #10 + + # numeric sec + var mySeq3 = @[1,2,3] + myDebug(mySeq3) #11 + + # seq had to grow + var mySeq4 = @["one","two","three"] + myDebug(mySeq4) #12 + + var myTable = initTable[int, string]() + myTable[4] = "four" + myTable[5] = "five" + myTable[6] = "six" + myDebug(myTable) #13 + + var myOtherTable = {"one": 1, "two": 2, "three": 3}.toTable + myDebug(myOtherTable) #14 + + var obj = MyObj(a: 1, b: "some string") + myDebug(obj) #15 + + var tup = ("hello", 42) + myDebug(tup) # 16 + + assert counter == 16 + + +testProc() diff --git a/tests/untestable/gdb/gdb_pretty_printer_test_run.sh b/tests/untestable/gdb/gdb_pretty_printer_test_run.sh new file mode 100755 index 000000000..411c68435 --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test_run.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +# Compile the test project with fresh debug information. +nim c --debugger:native --mm:orc --out:gdbNew gdb_pretty_printer_test_program.nim +echo "Running new runtime tests..." +# 2>&1 redirects stderr to stdout (all output in stdout) +gdb -x gdb_pretty_printer_test.py --batch-silent --args gdbNew 2>&1 + + +# Do it all again, but with old runtime +nim c --debugger:native --mm:refc --out:gdbOld gdb_pretty_printer_test_program.nim &> /dev/null +echo "Running old runtime tests" +gdb -x gdb_pretty_printer_test.py --batch-silent --args gdbOld 2>&1 diff --git a/tests/untestable/network/README.md b/tests/untestable/network/README.md new file mode 100644 index 000000000..173cf105f --- /dev/null +++ b/tests/untestable/network/README.md @@ -0,0 +1,8 @@ +This directory contains tests that require networking and cannot be run in CI. + +The tests can be run manually during development using: +```nim +./koch tests cat untestable/network/stdlib +``` + +The directory structure mimics tests/ diff --git a/tests/untestable/network/stdlib/tnet.nim b/tests/untestable/network/stdlib/tnet.nim new file mode 100644 index 000000000..cb0f38944 --- /dev/null +++ b/tests/untestable/network/stdlib/tnet.nim @@ -0,0 +1,16 @@ +discard """ +outputsub: "" +""" + +import net, nativesockets +import unittest + +suite "getPrimaryIPAddr": + test "localhost v4": + check getPrimaryIPAddr(parseIpAddress("127.0.0.1")) == parseIpAddress("127.0.0.1") + + test "localhost v6": + check getPrimaryIPAddr(parseIpAddress("::1")) == parseIpAddress("::1") + + test "v4": + check getPrimaryIPAddr() != parseIpAddress("127.0.0.1") diff --git a/tests/untestable/readme.markdown b/tests/untestable/readme.markdown new file mode 100644 index 000000000..de1ba9459 --- /dev/null +++ b/tests/untestable/readme.markdown @@ -0,0 +1,9 @@ +This directory contains integration tests which are not automatically executed +for various reasons: +- dependency on external services +- dependency on files / configuration / state of the local host +- tests that are extremely slow or require large amounts of memory or storage +- tests that spawn local daemons + +Integration tests can become stale very quickly. Automated ./koch tests are +strongly recommended. diff --git a/tests/untestable/thttpclient_ssl_disabled.nim b/tests/untestable/thttpclient_ssl_disabled.nim new file mode 100644 index 000000000..b95dad2c6 --- /dev/null +++ b/tests/untestable/thttpclient_ssl_disabled.nim @@ -0,0 +1,36 @@ +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Compile and run with: +## nim r --putenv:NIM_TESTAMENT_REMOTE_NETWORKING:1 -d:nimDisableCertificateValidation -d:ssl -p:. tests/untestable/thttpclient_ssl_disabled.nim + +from stdtest/testutils import enableRemoteNetworking +when enableRemoteNetworking and (defined(nimTestsEnableFlaky) or not defined(openbsd)): + import httpclient, net, unittest + + const expired = "https://expired.badssl.com/" + + doAssert defined(nimDisableCertificateValidation) + + suite "SSL certificate check - disabled": + + test "httpclient in insecure mode": + var ctx = newContext(verifyMode = CVerifyPeer) + var client = newHttpClient(sslContext = ctx) + let a = $client.getContent(expired) + + test "httpclient in insecure mode": + var ctx = newContext(verifyMode = CVerifyPeerUseEnvVars) + var client = newHttpClient(sslContext = ctx) + let a = $client.getContent(expired) + + test "net socket in insecure mode": + var sock = newSocket() + var ctx = newContext(verifyMode = CVerifyPeerUseEnvVars) + ctx.wrapSocket(sock) + sock.connect("expired.badssl.com", 443.Port) + sock.close diff --git a/tests/untestable/thttpclient_ssl_env_var.nim b/tests/untestable/thttpclient_ssl_env_var.nim new file mode 100644 index 000000000..3f25a6ff4 --- /dev/null +++ b/tests/untestable/thttpclient_ssl_env_var.nim @@ -0,0 +1,74 @@ +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Warning: this test performs external networking. +## Compile with: +## ./bin/nim c -d:ssl -p:. tests/untestable/thttpclient_ssl_env_var.nim +## +## Test with: +## SSL_CERT_FILE=BogusInexistentFileName tests/untestable/thttpclient_ssl_env_var +## SSL_CERT_DIR=BogusInexistentDirName tests/untestable/thttpclient_ssl_env_var + +import httpclient, unittest, os +from net import newSocket, newContext, wrapSocket, connect, close, Port, + CVerifyPeerUseEnvVars +from strutils import contains + +const + expired = "https://expired.badssl.com/" + good = "https://google.com/" + + +suite "SSL certificate check": + + test "httpclient with inexistent file": + if existsEnv("SSL_CERT_FILE"): + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) + var client = newHttpClient(sslContext=ctx) + checkpoint("Client created") + check client.getContent("https://google.com").contains("doctype") + checkpoint("Google ok") + try: + let a = $client.getContent(good) + echo "Connection should have failed" + fail() + except: + echo getCurrentExceptionMsg() + check getCurrentExceptionMsg().contains("certificate verify failed") + + elif existsEnv("SSL_CERT_DIR"): + try: + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) + var client = newHttpClient(sslContext=ctx) + echo "Should have raised 'No SSL/TLS CA certificates found.'" + fail() + except: + check getCurrentExceptionMsg() == + "No SSL/TLS CA certificates found." + + test "net socket with inexistent file": + if existsEnv("SSL_CERT_FILE"): + var sock = newSocket() + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) + ctx.wrapSocket(sock) + checkpoint("Socket created") + try: + sock.connect("expired.badssl.com", 443.Port) + fail() + except: + sock.close + check getCurrentExceptionMsg().contains("certificate verify failed") + + elif existsEnv("SSL_CERT_DIR"): + var sock = newSocket() + checkpoint("Socket created") + try: + var ctx = newContext(verifyMode=CVerifyPeerUseEnvVars) # raises here + fail() + except: + check getCurrentExceptionMsg() == + "No SSL/TLS CA certificates found." diff --git a/tests/untestable/thttpclient_ssl_remotenetwork.nim b/tests/untestable/thttpclient_ssl_remotenetwork.nim new file mode 100644 index 000000000..3cb759516 --- /dev/null +++ b/tests/untestable/thttpclient_ssl_remotenetwork.nim @@ -0,0 +1,230 @@ +# +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Test with: +## nim r --putenv:NIM_TESTAMENT_REMOTE_NETWORKING:1 -d:ssl -p:. --threads:on tests/untestable/thttpclient_ssl_remotenetwork.nim +## +## See https://github.com/FedericoCeratto/ssl-comparison/blob/master/README.md +## for a comparison with other clients. + +from stdtest/testutils import enableRemoteNetworking +when enableRemoteNetworking and (defined(nimTestsEnableFlaky) or not defined(windows) and not defined(openbsd)): + # Not supported on Windows due to old openssl version + import + httpclient, + net, + strutils, + threadpool, + unittest + + + type + # bad and dubious tests should not pass SSL validation + # "_broken" mark the test as skipped. Some tests have different + # behavior depending on OS and SSL version! + # TODO: chase and fix the broken tests + Category = enum + good, bad, dubious, good_broken, bad_broken, dubious_broken + CertTest = tuple[url:string, category:Category, desc: string] + + # badssl certs sometimes expire, set to false when that happens + when true: + const certificate_tests: array[0..54, CertTest] = [ + ("https://wrong.host.badssl.com/", bad, "wrong.host"), + ("https://captive-portal.badssl.com/", bad, "captive-portal"), + ("https://expired.badssl.com/", bad, "expired"), + ("https://google.com/", good, "good"), + ("https://self-signed.badssl.com/", bad, "self-signed"), + ("https://untrusted-root.badssl.com/", bad, "untrusted-root"), + ("https://revoked.badssl.com/", bad_broken, "revoked"), + ("https://pinning-test.badssl.com/", bad_broken, "pinning-test"), + ("https://no-common-name.badssl.com/", bad, "no-common-name"), + ("https://no-subject.badssl.com/", bad, "no-subject"), + ("https://sha1-intermediate.badssl.com/", bad, "sha1-intermediate"), + ("https://sha256.badssl.com/", good, "sha256"), + ("https://sha384.badssl.com/", bad, "sha384"), + ("https://sha512.badssl.com/", bad, "sha512"), + ("https://1000-sans.badssl.com/", bad, "1000-sans"), + ("https://10000-sans.badssl.com/", good_broken, "10000-sans"), + ("https://ecc256.badssl.com/", good_broken, "ecc256"), + ("https://ecc384.badssl.com/", good_broken, "ecc384"), + ("https://rsa2048.badssl.com/", good, "rsa2048"), + ("https://rsa8192.badssl.com/", dubious_broken, "rsa8192"), + ("http://http.badssl.com/", good, "regular http"), + ("https://http.badssl.com/", bad_broken, "http on https URL"), # FIXME + ("https://cbc.badssl.com/", dubious, "cbc"), + ("https://rc4-md5.badssl.com/", bad, "rc4-md5"), + ("https://rc4.badssl.com/", bad, "rc4"), + ("https://3des.badssl.com/", bad, "3des"), + ("https://null.badssl.com/", bad, "null"), + ("https://mozilla-old.badssl.com/", bad_broken, "mozilla-old"), + ("https://mozilla-intermediate.badssl.com/", dubious_broken, "mozilla-intermediate"), + ("https://mozilla-modern.badssl.com/", good, "mozilla-modern"), + ("https://dh480.badssl.com/", bad, "dh480"), + ("https://dh512.badssl.com/", bad, "dh512"), + ("https://dh1024.badssl.com/", dubious_broken, "dh1024"), + ("https://dh2048.badssl.com/", good, "dh2048"), + ("https://dh-small-subgroup.badssl.com/", bad_broken, "dh-small-subgroup"), + ("https://dh-composite.badssl.com/", bad_broken, "dh-composite"), + ("https://static-rsa.badssl.com/", dubious, "static-rsa"), + ("https://tls-v1-0.badssl.com:1010/", dubious, "tls-v1-0"), + ("https://tls-v1-1.badssl.com:1011/", dubious, "tls-v1-1"), + ("https://invalid-expected-sct.badssl.com/", bad, "invalid-expected-sct"), + ("https://hsts.badssl.com/", good, "hsts"), + ("https://upgrade.badssl.com/", good, "upgrade"), + ("https://preloaded-hsts.badssl.com/", good, "preloaded-hsts"), + ("https://subdomain.preloaded-hsts.badssl.com/", bad, "subdomain.preloaded-hsts"), + ("https://https-everywhere.badssl.com/", good, "https-everywhere"), + ("https://long-extended-subdomain-name-containing-many-letters-and-dashes.badssl.com/", good, + "long-extended-subdomain-name-containing-many-letters-and-dashes"), + ("https://longextendedsubdomainnamewithoutdashesinordertotestwordwrapping.badssl.com/", good, + "longextendedsubdomainnamewithoutdashesinordertotestwordwrapping"), + ("https://superfish.badssl.com/", bad, "(Lenovo) Superfish"), + ("https://edellroot.badssl.com/", bad, "(Dell) eDellRoot"), + ("https://dsdtestprovider.badssl.com/", bad, "(Dell) DSD Test Provider"), + ("https://preact-cli.badssl.com/", bad, "preact-cli"), + ("https://webpack-dev-server.badssl.com/", bad, "webpack-dev-server"), + ("https://mitm-software.badssl.com/", bad, "mitm-software"), + ("https://sha1-2016.badssl.com/", dubious, "sha1-2016"), + ("https://sha1-2017.badssl.com/", bad, "sha1-2017"), + ] + else: + const certificate_tests: array[0..0, CertTest] = [ + ("https://google.com/", good, "good") + ] + + + template evaluate(exception_msg: string, category: Category, desc: string) = + # Evaluate test outcome. Tests flagged as `_broken` are evaluated and skipped + let raised = (exception_msg.len > 0) + let should_not_raise = category in {good, dubious_broken, bad_broken} + if should_not_raise xor raised: + # we are seeing a known behavior + if category in {good_broken, dubious_broken, bad_broken}: + skip() + if raised: + # check exception_msg == "No SSL certificate found." or + doAssert exception_msg == "No SSL certificate found." or + exception_msg == "SSL Certificate check failed." or + exception_msg.contains("certificate verify failed") or + exception_msg.contains("key too small") or + exception_msg.contains("alert handshake failure") or + exception_msg.contains("bad dh p length") or + # TODO: This one should only triggers for 10000-sans + exception_msg.contains("excessive message size"), exception_msg + + else: + # this is unexpected + var fatal = true + var msg = "" + if raised: + msg = " $# ($#) raised: $#" % [desc, $category, exception_msg] + if "500 Internal Server Error" in exception_msg: + # refs https://github.com/nim-lang/Nim/issues/16338#issuecomment-804300278 + # we got: `good (good) raised: 500 Internal Server Error` + fatal = false + msg.add " (http 500 => assuming this is not our problem)" + else: + msg = " $# ($#) did not raise" % [desc, $category] + + if category in {good, dubious, bad} and fatal: + echo "D20210322T121353: error: " & msg + fail() + else: + echo "D20210322T121353: warning: " & msg + + + suite "SSL certificate check - httpclient": + + for i, ct in certificate_tests: + + test ct.desc: + var ctx = newContext(verifyMode=CVerifyPeer) + var client = newHttpClient(sslContext=ctx) + let exception_msg = + try: + let a = $client.getContent(ct.url) + "" + except: + getCurrentExceptionMsg() + + evaluate(exception_msg, ct.category, ct.desc) + + + + # threaded tests + + + type + TTOutcome = ref object + desc, exception_msg: string + category: Category + + proc run_t_test(ct: CertTest): TTOutcome {.thread.} = + ## Run test in a {.thread.} - return by ref + result = TTOutcome(desc:ct.desc, exception_msg:"", category: ct.category) + try: + var ctx = newContext(verifyMode=CVerifyPeer) + var client = newHttpClient(sslContext=ctx) + let a = $client.getContent(ct.url) + except: + result.exception_msg = getCurrentExceptionMsg() + + + suite "SSL certificate check - httpclient - threaded": + when defined(nimTestsEnableFlaky) or not defined(linux): # xxx pending bug #16338 + # Spawn threads before the "test" blocks + var outcomes = newSeq[FlowVar[TTOutcome]](certificate_tests.len) + for i, ct in certificate_tests: + let t = spawn run_t_test(ct) + outcomes[i] = t + + # create "test" blocks and handle thread outputs + for t in outcomes: + let outcome = ^t # wait for a thread to terminate + test outcome.desc: + evaluate(outcome.exception_msg, outcome.category, outcome.desc) + else: + echo "skipped test" + + # net tests + + + type NetSocketTest = tuple[hostname: string, port: Port, category:Category, desc: string] + # badssl certs sometimes expire, set to false when that happens + when true: + const net_tests:array[0..3, NetSocketTest] = [ + ("imap.gmail.com", 993.Port, good, "IMAP"), + ("wrong.host.badssl.com", 443.Port, bad, "wrong.host"), + ("captive-portal.badssl.com", 443.Port, bad, "captive-portal"), + ("expired.badssl.com", 443.Port, bad, "expired"), + ] + else: + const net_tests: array[0..0, NetSocketTest] = [ + ("imap.gmail.com", 993.Port, good, "IMAP") + ] + # TODO: ("null.badssl.com", 443.Port, bad_broken, "null"), + + + suite "SSL certificate check - sockets": + + for ct in net_tests: + + test ct.desc: + + var sock = newSocket() + var ctx = newContext() + ctx.wrapSocket(sock) + let exception_msg = + try: + sock.connect(ct.hostname, ct.port) + "" + except: + getCurrentExceptionMsg() + + evaluate(exception_msg, ct.category, ct.desc) diff --git a/tests/untestable/tpostgres.nim b/tests/untestable/tpostgres.nim new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/untestable/tpostgres.nim @@ -0,0 +1 @@ + diff --git a/tests/untestable/tssl.nim b/tests/untestable/tssl.nim new file mode 100644 index 000000000..fca6385f8 --- /dev/null +++ b/tests/untestable/tssl.nim @@ -0,0 +1,36 @@ +# +# Nim - SSL integration tests +# (c) Copyright 2017 Nim contributors +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# +## Warning: this test performs external networking. +## +## Test with: +## ./bin/nim c -d:ssl -p:. -r tests/untestable/tssl.nim +## ./bin/nim c -d:ssl -p:. --dynlibOverride:ssl --passL:-lcrypto --passL:-lssl -r tests/untestable/tssl.nim +## The compilation is expected to succeed with any new/old version of OpenSSL, +## both with dynamic and static linking. +## The "howsmyssl" test is known to fail with OpenSSL < 1.1 due to insecure +## cypher suites being used. + +import httpclient, os +from strutils import contains, toHex + +from openssl import getOpenSSLVersion + +when true: + echo "version: 0x" & $getOpenSSLVersion().toHex() + + let client = newHttpClient() + # hacky SSL check + const url = "https://www.howsmyssl.com" + let report = client.getContent(url) + if not report.contains(">Probably Okay</span>"): + let fn = getTempDir() / "sslreport.html" + echo "SSL CHECK ERROR, see " & fn + writeFile(fn, report) + quit(1) + + echo "done" |