. "$BST_LIB" # BST-TIMEOUT: 1200 # TWO python3 starts: a bare `import' and the probe, which # # takes ONE start for 104 measurements. # # # # THIS SUITE RUNS NO PROGRAM AND OPENS NO DEVICE. Unlike # # 99-python3-editor, which forks and execs an editor for # # every case, everything here is CPU and $TMPDIR: build a # # workbook in memory, deflate it into a ZIP, parse the XML # # back. So the cost is the interpreter start, twice, plus a # # few seconds of work -- 1200 is 400s per start (the # # per-start budget the neighbouring python3 suites use) # # plus 400s of slack for the ZIP and XML traffic. # # # # A suite-timeout red is a batch-wide cost, so if the # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed # # replaces this note in the test plan rather than in this # # comment, which would cost an image build to keep the # # shipped suite byte-identical. # # python3-xlsxwriter (XlsxWriter 3.1.9, meta-openembedded meta-python/ # recipes-devtools/python/python3-xlsxwriter_3.1.9.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE ACTUALLY IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # XlsxWriter WRITES AND NEVER READS. There is no reader anywhere in the # distribution -- 33 modules, all of them writers -- so NOTHING here can be # asserted by round-tripping through the library's own API. Every structural # case below opens the workbook as what it physically is, A ZIP CONTAINER OF # XML PARTS, using the interpreter's own `zipfile' and `ElementTree'. # # That is not a detour, it is the only instrument that can fail. Asserting # through the library's accessors would test that XlsxWriter agrees with # itself, which it does by construction: a writer that silently dropped a # merged range, a defined name or a second worksheet would pass such a test and # produce a file Excel opens wrong. Reading the XML is what makes the drop # visible, so every structural case names an element and an attribute rather # than an API return value. # # It is also why the recipe's RDEPENDS is exactly what it is. The ZIP writer IS # the payload, so python3-compression is load-bearing; `datetime' is how a date # becomes a serial number, `numbers' is the numeric type check on every write, # and `io' carries the StringIO the XML writer builds each part in. # # ============================================================================ # THE PORT ITSELF IS ONE LINE, AND THE INTERESTING PART IS WHY. # ============================================================================ # # PURE PYTHON, NO C, NO CONFIGURE. This image's python3 is one static binary # with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a module # carrying a compiled extension could not be imported here at all. XlsxWriter # carries none: `submodules-all-py' asserts that every one of the 33 modules # the import pulls in came from a `.py', which is the measurement behind the # claim rather than a reading of the recipe. # # THE RDEPENDS ARE ALL INTERPRETER SUB-PACKAGES. python3-compression, # python3-crypt, python3-datetime, python3-io and python3-numbers are splits of # the ported interpreter, not separate ports -- so this package has NO # dependency outside python3 itself, and `pkg-depends-exact' asserts the built # .deb's line verbatim rather than reading it off the recipe. # # ONE SCRIPT DOES LAND IN /usr/bin, and it is easy to miss. setup.py declares # `scripts=['examples/vba_extract.py']', so unlike the pure-module ports next # door this package ships an executable: /usr/bin/vba_extract.py. It is asserted # present and executable, and its `#!' line is asserted to have been rewritten # to the image's interpreter -- the sdist ships the bare `#!python' form, which # would be unrunnable here. # # ============================================================================ # THREE THINGS THE TICKET EXPECTED THAT ARE NOT WHAT IS THERE. # ============================================================================ # # These are recorded as what was MEASURED, not as what was expected. A suite # that asserted the expectation would be red against correct behaviour, and the # design doc would carry a guess nobody could later tell from a result. # # constant_memory DOES NOT PRODUCE THE SAME PARTS. The ticket expected parity # with the in-memory workbook. It cannot hold: a shared-string # table is only complete once every row has been seen, and # constant_memory has already flushed the rows. So it writes # STRINGS INLINE -- `t="inlineStr"' with `' -- and # emits NO xl/sharedStrings.xml at all. `cm-parts-missing' # names the one part that differs and `cm-a1-type' names the # encoding; what IS asserted to match is the data: the same # cell references, the same numeric cells, the same string # content reached the other way. # # WRITING TO A CLOSED WORKBOOK DOES NOT RAISE. `close()' sets `fileclosed' # and the container is already on disk, but `Worksheet.write' # never consults that flag: the call RETURNS 0 and the data is # silently discarded. `file-unchanged-after-late-write' # asserts the discard, which is the half a caller can actually # be harmed by. # # CLOSING TWICE DOES NOT RAISE EITHER. It emits a UserWarning through # `warnings.warn' and returns None. Asserted as a warning, # with its class, rather than as an exception. # # ============================================================================ # FIVE BEHAVIOURS A CALLER WILL GET WRONG. THESE ARE THE POINT OF THE SUITE. # ============================================================================ # # A DATE IS A NUMBER PLUS A FORMAT. There is no date type in the format at # all. `write_datetime' emits a bare `46268' with NO # `t' attribute and points the cell at a numFmt through its # style index, so a caller who forgets the format gets 46268 # in the cell. `a7-value', `a7-no-type-attr' and # `custom-numfmts' are the three halves of that one fact. # # A FORMULA IS NOT EVALUATED, AND THE CACHED VALUE IS A LIE. XlsxWriter has no # calculation engine: it writes `A2+A3' beside a CACHED # `0', and Excel recalculates on open. A consumer that # is not Excel -- which is every consumer on this image -- # reads 0. Asserted as 0 deliberately. # # THE LEADING `=' IS STRIPPED. `write_formula('A6', '=A2+A3')' stores `A2+A3'; # the `=' is the spelling a user types, not what the format # holds. # # A MERGED RANGE WITH NO FORMAT LEAVES ITS OTHER CELLS PHYSICALLY ABSENT. # `merge_range' pads the rest of the range with blank cells # carrying the format -- and `_write_blank' RETURNS EARLY when # the format is None. So `merge_range('D1:E1', data)' declares # `' over a cell E1 that is not in the # sheet. Excel opens it; a consumer walking `' elements sees # a merge whose second cell does not exist. Both shapes are # measured -- D1:E1 without a format, F1:G1 with one. # # THE DUPLICATE-SHEETNAME CHECK IS CASE-INSENSITIVE, like Excel's. `Keep' and # `KEEP' collide, so a caller who thinks they are comparing # strings gets a raise on what looks to them like a new name. # # ============================================================================ # HOW THE CASES ARE SHAPED. # ============================================================================ # # One `import' on its own, then ONE probe start for everything else. A python3 # start on this arm is tens of seconds, so a case-per-start suite would be a # half-hour run; $BST_DATA/python3-xlsxwriter-probe.py takes one start for 104 # measurements and prints them as `key=value' lines the shell reads back. # # `probe-run' is bounded and measured BEFORE any value is read, so an # interpreter that never started, or a probe that died partway, shows up as one # named fact instead of as a hundred unexplained reds. # # EVERY FIXTURE IS WRITTEN BY THE PROBE. Nothing ships, nothing is downloaded, # and the tree is removed at the end -- `workdir-gone' says so. The suite # reaches no device, no peer and no terminal, which is what lets this port close # on its suite with no BRINTOS-780 follow-on. P=python3-xlsxwriter bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/xlsxwriter DI=$SITE/XlsxWriter-3.1.9.dist-info PKGLIST=/var/lib/dpkg/info/python3-xlsxwriter.list SCRIPT=/usr/bin/vba_extract.py PROBE=$BST_DATA/python3-xlsxwriter-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-xlsxwriter-suite.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ what shipped --- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" check $P/is-a-package test -f "$PKG/__init__.py" check $P/not-a-flat-module test ! -e "$SITE/xlsxwriter.py" # The modules the port turns on: the writer front end, the ZIP packager, and the # XML writer every part is built through. check $P/module-workbook test -s "$PKG/workbook.py" check $P/module-worksheet test -s "$PKG/worksheet.py" check $P/module-packager test -s "$PKG/packager.py" check $P/module-xmlwriter test -s "$PKG/xmlwriter.py" check $P/module-exceptions test -s "$PKG/exceptions.py" check $P/module-utility test -s "$PKG/utility.py" check $P/pycache-init test -s "$PKG/__pycache__/__init__.cpython-312.pyc" check $P/pycache-workbook test -s "$PKG/__pycache__/workbook.cpython-312.pyc" # The sdist's own test tree and docs must not have been packaged. check $P/no-tests-dir test ! -d "$PKG/test" check $P/no-examples test ! -d "$SITE/examples" # ----------------------------------------------------- the shipped script ---- # # setup.py declares `scripts=['examples/vba_extract.py']', so this package -- # alone among the pure-Python ports next door -- puts a file in /usr/bin. Its # `#!' is `#!python' in the sdist, which is not runnable anywhere; setuptools3 # rewrites it at install, and that rewrite is what is asserted. check $P/script-present test -s "$SCRIPT" check $P/script-executable test -x "$SCRIPT" check_out $P/script-shebang "#!/usr/bin/env python3" head -n 1 "$SCRIPT" check_eq $P/script-no-bare-python-shebang "" \ "$(head -n 1 "$SCRIPT" | grep -x '#!python')" # With no argument it prints its usage and falls off the end of the branch into # a bare `exit()' -- exit(None), so the status is 0, NOT the non-zero a usage # path is usually written with. Measured, not read: the phase-A prediction here # was rc=1 and the image reported rc=0. Asserting the usage TEXT rather than the # bare status is what keeps the case meaningful, since 0 is also what a # successful extraction returns; check_out requires both, so an image where the # script could not start at all is still distinguishable from one where it ran # and complained. check_out $P/script-usage-rc "Usage: vba_extract file.xlsm" python3 "$SCRIPT" # ------------------------------------------------------- what dpkg thinks ---- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" check_out $P/pkg-lists-script "$SCRIPT" cat "$W/pkgf" # Every packaged FILE is a .py, a .pyc, dist-info metadata or the one script. # This is the packaging half of "pure Python": a .so slipping in would be # unimportable on this arm, and it would arrive silently. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in "$SCRIPT") continue ;; *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: XlsxWriter" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 3.1.9" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/dist-info-no-py2-tag "" \ "$(grep '^Tag: py2' "$DI/WHEEL" 2>/dev/null)" check_out $P/dist-info-top-level "xlsxwriter" cat "$DI/top_level.txt" sed -n '/^Package: python3-xlsxwriter$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 3.1.9-r0" cat "$W/st" # The RDEPENDS are five interpreter sub-packages and nothing else -- this # package depends on no other port. Asserted against the built .deb rather than # read off the recipe. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact \ "Depends: python3-compression, python3-core, python3-crypt, python3-datetime, python3-io, python3-numbers" \ "$(cat "$W/dep")" # The interpreter sub-packages the payload actually reaches for, present as # directories rather than only as dpkg lines: a Depends satisfied on paper by a # package whose files did not land would fail at the first import. check $P/stdlib-zipfile test -s /usr/lib/python3.12/zipfile/__init__.py check $P/stdlib-zlib-or-builtin python3 -c 'import zlib' check $P/stdlib-datetime test -s /usr/lib/python3.12/datetime.py check $P/stdlib-etree test -s /usr/lib/python3.12/xml/etree/ElementTree.py # ------------------------------------------------------------- the import ---- check $P/import python3 -c 'import xlsxwriter' # A clean import writes nothing to stderr. A DeprecationWarning or a # site-packages complaint here would be invisible to the case above. check_eq $P/import-quiet "" \ "$(python3 -c 'import xlsxwriter' 2>&1)" # ---------------------------------------------------------------- probe ------ if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "104" "$(pv emitted_count)" check_eq $P/probe-quiet "" "$(cat "$OUT.err" 2>/dev/null)" # ------------------------------------------------------------- identity ------ check_eq $P/version-attr "3.1.9" "$(pv version_attr)" check_eq $P/version-upper-attr "3.1.9" "$(pv version_upper_attr)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/package-dir-answered "$PKG" "$(pv package_dir)" # The top-level namespace is FIVE LINES: a version, its uppercase alias, and # `from .workbook import Workbook'. So there is no __all__, `Worksheet' and # `Format' are NOT importable from the top level, and the exported `Workbook' is # the class itself rather than a re-export of a copy. check_eq $P/has-dunder-all "no" "$(pv has_dunder_all)" check_eq $P/toplevel-workbook-is-class "yes" "$(pv toplevel_workbook_is_class)" check_eq $P/no-toplevel-worksheet "no" "$(pv has_worksheet_toplevel)" check_eq $P/no-toplevel-format "no" "$(pv has_format_toplevel)" # PURE PYTHON, MEASURED. 33 modules pulled in by the import, every one of them # from a .py -- there is no compiled extension anywhere, which is the property # that makes this package portable to an interpreter with no dynamic loader. check_eq $P/submodules-loaded "33" "$(pv submodules_loaded)" check_eq $P/submodules-all-py "yes" "$(pv submodules_all_py)" # ------------------------------------------------- the exception contract ---- check_eq $P/exc-base-is-exception "yes" "$(pv exc_base_is_exception)" check_eq $P/exc-input-base "yes" "$(pv exc_input_base)" check_eq $P/exc-file-base "yes" "$(pv exc_file_base)" check_eq $P/exc-invalid-name-is-input "yes" "$(pv exc_invalid_name_is_input)" check_eq $P/exc-dup-name-is-input "yes" "$(pv exc_dup_name_is_input)" check_eq $P/exc-overlap-is-input "yes" "$(pv exc_overlap_is_input)" check_eq $P/exc-create-is-file "yes" "$(pv exc_create_is_file)" check_eq $P/exc-size-is-file "yes" "$(pv exc_size_is_file)" # ---------------------------------------------------- the container itself --- check_eq $P/workbook-written "yes" "$(pv main_written)" check_eq $P/workbook-is-zip "yes" "$(pv main_is_zip)" check_eq $P/zip-integrity-ok "yes" "$(pv main_testzip_ok)" # DEFLATED, NOT STORED -- this is python3-compression doing its job. A ZIP whose # members were all stored would still open, so a size check would not catch a # zlib that was silently unavailable; the per-member compress_type does. check_eq $P/zip-all-deflated "yes" "$(pv main_all_deflated)" # The parts the format requires, in the order the packager writes them, asserted # as one literal rather than as a dozen presence tests: a writer that dropped a # part is caught, and so is one that added an unexpected one. check_eq $P/parts-exact \ "[Content_Types].xml,_rels/.rels,docProps/app.xml,docProps/core.xml,xl/_rels/workbook.xml.rels,xl/sharedStrings.xml,xl/styles.xml,xl/theme/theme1.xml,xl/workbook.xml,xl/worksheets/sheet1.xml,xl/worksheets/sheet2.xml" \ "$(pv main_parts)" check_eq $P/part-count "11" "$(pv main_part_count)" # [Content_Types].xml MUST BE FIRST in an OPC container. check_eq $P/first-part "[Content_Types].xml" "$(pv main_first_part)" check_eq $P/content-types-overrides "8" "$(pv content_types_overrides)" check_eq $P/content-types-has-sheet1 "yes" "$(pv content_types_has_sheet1)" check_eq $P/root-rels-targets \ "docProps/app.xml,docProps/core.xml,xl/workbook.xml" \ "$(pv root_rels_targets)" # ---------------------------------------------------- workbook.xml ---------- check_eq $P/sheet-names "Data,Second" "$(pv workbook_sheet_names)" check_eq $P/sheet-ids "1,2" "$(pv workbook_sheet_ids)" # A DEFINED NAME SCOPED TO A SHEET carries localSheetId, and the sheet # qualifier is stripped from the name itself -- `'Data'!answer' is stored as # `answer' with localSheetId="0". A caller who expected the qualifier to survive # into the name would not find it. check_eq $P/defined-name-count "1" "$(pv defined_name_count)" check_eq $P/defined-name-name "answer" "$(pv defined_name_name)" check_eq $P/defined-name-localsheet "0" "$(pv defined_name_localsheet)" check_eq $P/defined-name-value "'Data'!\$A\$2" "$(pv defined_name_value)" # ------------------------------------------------- sheet1.xml, cell by cell -- # # One cell per TYPE, because the `t' attribute is where the writer's type # decision becomes visible. check_eq $P/string-is-shared "s" "$(pv a1_type)" check_eq $P/string-index "0" "$(pv a1_index_value)" check_eq $P/string-resolves "alpha" "$(pv a1_resolves)" # A NUMBER CARRIES NO `t' AT ALL -- the absence is the type. check_eq $P/integer-has-no-type "" "$(pv a2_type)" check_eq $P/integer-value "42" "$(pv a2_value)" check_eq $P/float-value "3.5" "$(pv a3_value)" check_eq $P/boolean-type "b" "$(pv a4_type)" check_eq $P/boolean-true "1" "$(pv a4_value)" check_eq $P/boolean-false "0" "$(pv a5_value)" # THE LEADING `=' IS STRIPPED, and the cached value is 0 because XlsxWriter has # no calculation engine. Excel recalculates on open; nothing on this image does. check_eq $P/formula-stored "A2+A3" "$(pv a6_formula)" check_eq $P/formula-cached-zero "0" "$(pv a6_cached_value)" # A DATE IS A SERIAL NUMBER PLUS A FORMAT. 2026-09-03 is day 46268 of the 1900 # system; the cell carries no type attribute and the format lives in styles.xml. check_eq $P/date-serial "46268" "$(pv a7_value)" check_eq $P/date-has-no-type "yes" "$(pv a7_has_no_type_attr)" check_eq $P/date-numfmt "yyyy-mm-dd" "$(pv custom_numfmts)" # THE SHARED-STRING TABLE IS THE POINT OF THE FORMAT. A1 and B1 hold the same # text and must therefore hold the same INDEX; a writer that emitted the literal # twice would still open in Excel, so equality of the two refs is the only way # the table is visibly working. check_eq $P/shared-strings "alpha,merged,fmerged,on-two" "$(pv shared_strings)" check_eq $P/shared-strings-count "4" "$(pv shared_strings_count)" check_eq $P/duplicate-string-shared "yes" "$(pv b1_shares_with_a1)" check_eq $P/duplicate-string-index "0" "$(pv b1_index_value)" # ------------------------------------------------------------ merged ranges -- check_eq $P/merge-count "2" "$(pv merge_count)" check_eq $P/merge-refs "D1:E1,F1:G1" "$(pv merge_refs)" check_eq $P/merge-topleft-is-string "s" "$(pv merge_topleft_type)" # NO FORMAT, NO PADDING: E1 is declared by and is not in the sheet. check_eq $P/merge-unformatted-pad-absent "no" "$(pv merge_unformatted_pad_present)" # WITH a format the padding IS written -- a blank cell carrying the style. check_eq $P/merge-formatted-topleft-is-string "s" "$(pv merge_formatted_topleft_type)" check_eq $P/merge-formatted-pad-present "yes" "$(pv merge_formatted_pad_present)" check_eq $P/merge-formatted-pad-blank "" "$(pv merge_formatted_pad_value)" check_eq $P/merge-formatted-pad-styled "yes" "$(pv merge_formatted_pad_styled)" # ---------------------------------------------------------- second worksheet -- check_eq $P/sheet2-cell-count "1" "$(pv sheet2_cell_count)" # ------------------------------------------- sharedStrings.xml is conditional -- # # The packager writes the part only if the table is non-empty, so its presence # is a fact about the DATA rather than about the format. A suite that always # expected it would be asserting its own fixture. check_eq $P/no-strings-no-table "no" "$(pv nostr_has_sharedstrings)" check_eq $P/no-strings-parts \ "[Content_Types].xml,_rels/.rels,docProps/app.xml,docProps/core.xml,xl/_rels/workbook.xml.rels,xl/styles.xml,xl/theme/theme1.xml,xl/workbook.xml,xl/worksheets/sheet1.xml" \ "$(pv nostr_parts)" # ------------------------------------------------------- constant_memory ------ # # THE TICKET EXPECTED PARITY AND THERE IS NONE, for a reason that is the whole # mechanism: constant_memory flushes each row as it is written, so the # shared-string table can never be completed and strings go INLINE instead. # What is asserted is the data -- same cell references, same numeric cells, same # string content by a different encoding -- and the ONE part that differs, by # name. check_eq $P/cm-parts-differ "no" "$(pv cm_parts_match)" check_eq $P/cm-parts-missing "xl/sharedStrings.xml" "$(pv cm_parts_missing)" check_eq $P/cm-parts-no-extra "" "$(pv cm_parts_extra)" check_eq $P/cm-cell-refs-match "yes" "$(pv cm_cell_refs_match)" check_eq $P/cm-string-inline "inlineStr" "$(pv cm_a1_type)" check_eq $P/cm-string-value "alpha" "$(pv cm_a1_value)" check_eq $P/im-string-shared "s" "$(pv im_a1_type)" check_eq $P/cm-numbers-match "yes" "$(pv cm_numeric_cells_match)" check_eq $P/cm-integer "42" "$(pv cm_a2_value)" check_eq $P/cm-float "3.5" "$(pv cm_a3_value)" # close() REMOVES THE ROW SPOOL. On a guest whose $TMPDIR is a small tmpfs a # leak here would land on whatever ran next rather than on this suite. check_eq $P/cm-tmpdir-clean "yes" "$(pv cm_tmpdir_clean)" # --------------------------------------------------------------- in_memory --- # # in_memory keeps the parts in RAM until close() instead of spooling them to # $TMPDIR -- the option a guest with an absent or read-only temporary directory # needs. It must produce the SAME container, and unlike constant_memory it does. check_eq $P/in-memory-written "yes" "$(pv in_memory_written)" check_eq $P/in-memory-is-zip "yes" "$(pv in_memory_is_zip)" check_eq $P/in-memory-parts-match "yes" "$(pv in_memory_parts_match)" # ------------------------------------------------------------- error paths --- # # All of these are raised by _check_sheetname BEFORE any XML is written, which # is why one never-closed workbook can provoke every one of them. check_eq $P/name-too-long "!InvalidWorksheetName" "$(pv name_too_long)" check_eq $P/name-31-is-legal "yes" "$(pv name_exactly_31_ok)" check_eq $P/name-slash-rejected "!InvalidWorksheetName" "$(pv name_invalid_char)" check_eq $P/name-bracket-rejected "!InvalidWorksheetName" "$(pv name_invalid_bracket)" check_eq $P/name-apostrophe-rejected "!InvalidWorksheetName" "$(pv name_leading_apostrophe)" check_eq $P/name-duplicate "!DuplicateWorksheetName" "$(pv name_duplicate)" # CASE-INSENSITIVE, like Excel's own check. check_eq $P/name-duplicate-case "!DuplicateWorksheetName" "$(pv name_duplicate_case)" # A SINGLE-CELL MERGE IS REFUSED WITHOUT AN EXCEPTION -- warn and return. A # caller merging a computed range that collapsed to one cell gets no error and # no merge. check_eq $P/merge-single-cell-returns-none "None" "$(pv merge_single_cell)" check_eq $P/merge-single-cell-warns "yes" "$(pv merge_single_cell_warned)" check_eq $P/merge-single-cell-warning-class "UserWarning" "$(pv merge_single_cell_warning_class)" # AN OVERLAP DOES RAISE -- Excel treats it as file corruption, so it is the one # range error that is fatal. The first merge's 0 is what makes this a statement # about overlap rather than about the call failing twice. check_eq $P/merge-first-succeeds "0" "$(pv merge_first_ok)" check_eq $P/merge-overlap-raises "!OverlappingRange" "$(pv merge_overlap)" # ---- THE THREE THE TICKET GOT WRONG, recorded as measured ------------------- check_eq $P/write-after-close-returns-zero "0" "$(pv write_after_close)" check_eq $P/add-worksheet-after-close-ok "Worksheet" "$(pv add_worksheet_after_close)" # The half a caller can be harmed by: the late write is SILENTLY DISCARDED. check_eq $P/late-write-not-persisted "yes" "$(pv file_unchanged_after_late_write)" check_eq $P/close-twice-returns-none "None" "$(pv close_twice)" check_eq $P/close-twice-warns "yes" "$(pv close_twice_warned)" check_eq $P/close-twice-warning-class "UserWarning" "$(pv close_twice_warning_class)" # AN IOError IS TRANSLATED at close(), not at construction -- the file is not # opened until then. The translation is the only reason a caller can catch one # XlsxWriter exception rather than two unrelated ones. check_eq $P/bad-path-raises-filecreateerror "!FileCreateError" "$(pv bad_path_close)" # ---------------------------------------------------------------- utility ---- # # The cell-reference helpers are ZERO-INDEXED while the references they produce # are one-indexed -- the off-by-one every consumer of this library writes once. check_eq $P/xl-rowcol-to-cell "A1" "$(pv xl_rowcol_to_cell)" check_eq $P/xl-rowcol-to-cell-abs "\$C\$2" "$(pv xl_rowcol_to_cell_abs)" check_eq $P/xl-cell-to-rowcol "1,2" "$(pv xl_cell_to_rowcol)" # Column 26 is AA, not Z: the numbering is zero-based, so Z is 25. check_eq $P/xl-col-to-name-26 "AA" "$(pv xl_col_to_name_26)" check_eq $P/xl-range "A1:B2" "$(pv xl_range)" check_eq $P/xl-range-abs "\$A\$1:\$B\$2" "$(pv xl_range_abs)" # ---------------------------------------------------------------- hygiene ---- check_eq $P/probe-workdir-gone "yes" "$(pv workdir_gone)" check_eq $P/probe-cwd-restored "yes" "$(pv cwd_restored)" rm -rf "$W"