Compare commits

...

3 Commits

Author SHA1 Message Date
arda.aydin@ontrol.com.tr
7a99fa1b87 Track testprogram/ again, add claudeCodeChatLog.md
The test pair verify.py runs against is back in the repo, so a fresh clone
can run the built-in checks. Conversion output (*-sab2sax-*) stays ignored,
so converting inside testprogram/ still leaves the tree clean.

claudeCodeChatLog.md records how this got built: what was asked for, the
decisions worth remembering (plain paths in system.properties, the port
source, Java float formatting, the f4-rounded manifest default, why a sab
cannot return runtime props) and what is still untested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:59:14 +03:00
arda.aydin@ontrol.com.tr
2dcc1704a6 Add a window, and run.py as the entry point
gui.py: one button per direction, "SAB file to SAX file" and "SAX file to
SAB file". The button decides which extension the chooser offers and which
way the conversion goes. The log names the chosen file and its folder before
the work starts, then the output file and folder when it finishes, each value
on its own line. Output lands next to the input under the timestamped name,
so there is no save dialog and nothing is overwritten. Sedona home sits at
the top with a Change... button that validates manifests/ and rewrites
system.properties.

run.py: no arguments opens the window, any argument goes to the command line
converter. pythonw run.py for a shortcut with no console.

config: NAME/VERSION are now "SAB - SAX Sedona Files Converter 0.0.0.002",
carried by the window title, every dialog title and the CLI banner. Adds
choose_sab_input() - the existing choose_sab_file() is the save dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:05:15 +03:00
arda.aydin@ontrol.com.tr
1fdfed90dd verify.py: round-trip any app you pick, with a file chooser
Takes a file now -- python verify.py [app.sax|app.sab] [sedona_home] -- and
opens the same chooser as the converter when none is given. Direction follows
the extension:

  .sab  sab -> sax -> sab            must come back byte-identical
  .sax  sax -> sab -> sax -> sab     the two sabs must be identical

The sax case compares the binaries, not the two texts: a sab carries no
runtime prop values and no formatting, so a regenerated sax may legitimately
differ from a hand-written original. That is printed as a note, not a failure.

Cancelling the chooser falls back to the built-in testprogram/ regression,
now four checks (both sedonac comparisons plus both round trips).

Malformed input fails with a one-line reason and exit 1 instead of a
traceback, and sax.py reports a broken parent/child/sibling id as
"comp 0 app: missing child 254" rather than a bare KeyError. Sedona home is
resolved lazily so importing verify opens no dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:30:52 +03:00
10 changed files with 1740 additions and 50 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
__pycache__/
*.pyc
testprogram/
*-sab2sax-*.sab
*-sab2sax-*.sax

View File

@ -10,13 +10,46 @@ runtime lib).
Verified byte-identical to `sedonac.exe` in both directions on a 189-component / 217-link app across
18 kits.
## Usage
## Run it
python sab2sax.py [in.sax|in.sab] [out]
python run.py the window
pythonw run.py the window with no console behind it
python run.py app.sab [out] straight to the command line converter
`run.py` is the single entry point: no arguments opens the GUI, any argument is handed to the
command line converter. Double-click it (or a shortcut to `pythonw run.py`) and you get the window.
## The window
python run.py (or: python gui.py)
Title bar carries the name and version (`SAB - SAX Sedona Files Converter 0.0.0.002`). One button
per direction — **SAB file to SAX file** and **SAX file to SAB file** — and the button you press
decides which extension the file chooser offers. The log shows the chosen file and its folder
*before* the conversion runs, then the output file and its folder when it finishes:
sab -> sax
INPUT FILE:
app.sab
INPUT PATH:
C:\apps
OUTPUT FILE:
app-sab2sax-20260728-140609.sax
OUTPUT PATH:
C:\apps
wrote 61294 bytes
The result is written next to the input under that timestamped name, so nothing is overwritten and
no save dialog gets in the way. Sedona home is shown at the top with a `Change...` button that
rewrites `system.properties`.
## The command line
python run.py [in.sax|in.sab] [out] (or: python sab2sax.py ...)
Like `sedonac`, the direction follows the input's extension: feed it a `.sax` and you get a `.sab`,
feed it a `.sab` and you get a `.sax`. Both arguments are optional, so you can run it by
double-click:
feed it a `.sab` and you get a `.sax`. Both arguments are optional`sab2sax.py` on its own asks for
what it needs:
* no input → a file chooser opens, listing `.sax` and `.sab`;
* no output → a save dialog opens next to the input, pre-filled with a timestamped name:
@ -63,36 +96,56 @@ set the key by hand instead.
## Version
`config.NAME` / `config.VERSION` — currently `sab2sax 0.0.0.001`. Every dialog title bar reads
`sab2sax 0.0.0.001 - Select the .sax or .sab application file to convert`, and the CLI prints the
same line when it starts, so it is obvious which build produced a file. Bump it in
[config.py](config.py) and both follow.
`config.NAME` / `config.VERSION` — currently `SAB - SAX Sedona Files Converter 0.0.0.002`. The window
title and every dialog title bar carry it; a chooser reads `SAB - SAX Sedona Files Converter
0.0.0.002 - Select the .sax or .sab application file to convert`, and the CLI prints the same line
when it starts, so it is obvious which build produced a file. Bump it in [config.py](config.py) and
all of them follow.
## Verify
python verify.py [sedona_home]
python verify.py [app.sax|app.sab] [sedona_home]
Without an argument the home comes from `system.properties`. Three checks, run against a
`test_normal.sax` / `test_normal_sedonac.sab` pair in `testprogram/`. That folder is **not** in the
repo (gitignored) — drop the pair in yourself, otherwise verify prints `SKIP` and exits 0:
Point it at any app of yours and it round-trips it, direction by extension:
test_normal.sax -> sab vs test_normal_sedonac.sab
test_normal_sedonac.sab -> sax vs test_normal.sax
test_normal_sedonac.sab -> sax -> sab back to the same bytes
app.sab -> sab -> sax -> sab must come back byte-identical
app.sax -> sax -> sab -> sax -> sab the two sabs must be identical
The `.sax` case compares the two **binaries**, not the two texts, because going through a `.sab` is
lossy on purpose — runtime prop values and formatting are not in the binary. If the regenerated
`.sax` differs from the original, that is printed as a note rather than a failure. (For a `.sax`
sedonac itself wrote, they do match.)
With no file argument a chooser opens, listing `.sax` and `.sab`. Cancel it and the built-in
regression runs instead, against the `test_normal.sax` / `test_normal_sedonac.sab` pair checked in
under `testprogram/`. Remove them and verify prints `SKIP` and exits 0 instead of failing:
test_normal.sax -> sab vs test_normal_sedonac.sab
test_normal_sedonac.sab -> sax vs test_normal.sax
both round trips as above
`check_encode(sax)` / `check_decode(sab)` with no reference argument run `bin/sedonac.exe` on a copy
of the input and diff against that instead — a live comparison, if you have a JRE it will start
under.
Exit code is 0 on pass, 1 on failure. A malformed file fails with a one-line reason
(`comp 0 app: missing child 254`) rather than a traceback.
## Files
run.py entry point: window with no arguments, CLI with them
gui.py the window: a button per direction
sab2sax.py the CLI: dispatches on the input extension
sab.py sax -> sab encoder
sax.py sab -> sax decoder
config.py system.properties read/write, folder + file choosers
verify.py regression check against sedonac's output, both directions
system.properties sedona.home
testprogram/ local only, gitignored: test apps to run verify.py against
testprogram/ test apps verify.py runs against
claudeCodeChatLog.md how this got built, and why
Conversion output (`*-sab2sax-*.sax`, `*-sab2sax-*.sab`) is gitignored, so converting inside
`testprogram/` leaves the repo clean.
## Format

86
claudeCodeChatLog.md Normal file
View File

@ -0,0 +1,86 @@
# Claude Code session log
What was asked for, what was built, and the decisions behind it. Written 2026-08-13, covering the
work that produced `ec4019e` .. `2dcc170` (tags `v0.0.0.001`, `v0.0.0.002`).
Starting point: `sab.py`, a pure-Python `.sax` -> `.sab` encoder taking `<app.sax> <sedona_home>
<out.sab>` on the command line, plus `verify.py`.
## What was asked, in order
1. **Sedona home into `system.properties`**, with a folder chooser on first run, and a file chooser
for the input.
2. **Plain paths, not java-escaped**, in that properties file.
3. **Timestamped output names**`[name]-sab2sax-20260728-140609`.
4. **The reverse direction**, `.sab` -> `.sax`, direction chosen by the input's extension.
5. **A version**, shown in dialog titles.
6. **`testprogram/` out of the repo** — later reversed: the test pair is checked in again, while
conversion output (`*-sab2sax-*`) stays gitignored.
7. **A file chooser in `verify.py`** and a round-trip check on whatever is chosen.
8. **A window** with a button per direction, showing the chosen file before converting and the
output afterwards.
9. **`run.py`** as the entry point; log labels in capitals, values on their own lines.
## What exists now
run.py entry point: window with no arguments, CLI with them
gui.py the window: a button per direction
sab2sax.py the CLI: dispatches on the input extension
sab.py sax -> sab encoder
sax.py sab -> sax decoder
config.py system.properties, dialogs, NAME/VERSION
verify.py round trips + comparison against sedonac output
system.properties sedona.home
testprogram/ test apps verify.py runs against
`sedona.home` lives in `system.properties`. Missing or pointing somewhere without `manifests/` and a
folder chooser opens, then the answer is written back. Output files are always written next to the
input as `<stem>-sab2sax-<YYYYMMDD-HHMMSS>.<ext>`, so a run never overwrites the previous one.
## Decisions worth remembering
**Plain paths in `system.properties`.** Java's `Properties` (what sedonac reads it with) treats `\`
as an escape and doubles them up; we only read the file ourselves, so we write plain Windows paths.
The parser collapses `\\` to `\` and keeps every other backslash literal — both forms load, and
`C:\niagara` never becomes a newline.
**The decoder is a port, not a guess.** `sax.py` follows `OfflineApp.decodeAppBinary` +
`encodeAppXml` and `OfflineComponent`/`OfflineLink` from
`sedonaSrcOrginal/sedona/src/sedona/offline/`. The tree is rebuilt from the
`firstChild`/`nextSibling` id chain; the XML is written the way `XWriter` does it — two-space
indent, a `<!-- /path -->` comment above every `<comp>`, `&#x..;` escaping.
**Java float formatting.** `Value.encodeString` for a float is `java.lang.Float.toString`: shortest
digits that round-trip, always a decimal point, `E` form outside `[1e-3, 1e7)`. `sax.java_float`
reproduces it. It matches modern Java rather than the old JDK's extra-digit quirk on subnormals
(`1.4E-45` vs our `1.0E-45`) — only reachable with denormal floats.
**The manifest default is a 32-bit float too.** A prop equal to its default is not written to the
`.sax`. Comparing a decoded `float` against a default parsed as a Python double makes `0.1` look
different from `0.1`, and every `minDelta="0.1"` gets written where sedonac omits it. Rounding the
default to `f4` first was the last diff between "close" and "identical".
**A `.sab` cannot give back runtime props.** Only config props are in the binary. sedonac has the
same hole. That is why a `.sax` round trip compares the two *binaries*, not the two texts.
## Verification
- `test_normal.sax` -> sab, byte-identical to `test_normal_sedonac.sab` (7852 bytes)
- `test_normal_sedonac.sab` -> sax, byte-identical to `test_normal.sax` (61294 bytes)
- both round trips clean: `sab -> sax -> sab`, and `sax -> sab -> sax -> sab`
A live `sedonac.exe` comparison was attempted and could not run here — it fails with `Cannot load
library: C:\Program Files\Zulu\zulu-21-jre\bin\server\jvm.dll`. `verify.check_encode(sax)` /
`check_decode(sab)` with no reference argument still do that comparison on a machine with a JRE
sedonac will start under.
Malformed input fails with one line and exit 1, not a traceback: `comp 0 app: missing child 254`.
## Where to pick up
- Untested paths are unchanged from before: action overrides, `Buf`-typed props, non-ASCII strings,
and apps whose ids exceed the 256-entry lookup table.
- `verify.py` prints `SKIP` and exits 0 when `testprogram/` is empty rather than failing; the test
pair is checked in, so it normally runs.
- Version lives in `config.py` (`NAME`, `VERSION`). The window title, every dialog title and the CLI
banner all read from it.

View File

@ -14,8 +14,8 @@ chooser is shown and the answer is written back to `system.properties`.
"""
import os, sys
NAME = "sab2sax"
VERSION = "0.0.0.001"
NAME = "SAB - SAX Sedona Files Converter"
VERSION = "0.0.0.002"
HERE = os.path.dirname(os.path.abspath(__file__))
PROPS = os.path.join(HERE, "system.properties")
@ -169,7 +169,15 @@ def choose_input_file(initial=None):
def choose_sax_file(initial=None):
return ask_open_file("Select the .sax application file", [SAX, ANY], initial)
"""Open a .sax -- the input of a sax -> sab conversion."""
return ask_open_file("Select the .sax application file to convert to .sab",
[SAX, ANY], initial)
def choose_sab_input(initial=None):
"""Open a .sab -- the input of a sab -> sax conversion."""
return ask_open_file("Select the .sab application file to convert to .sax",
[SAB, ANY], initial)
def choose_sab_file(initialfile=None, initial=None):

166
gui.py Normal file
View File

@ -0,0 +1,166 @@
"""A window with two buttons, one per direction.
python gui.py
Pick a direction, pick a file; the log shows what was chosen before the
conversion runs and where the result landed afterwards. Output goes next to the
input under a timestamped name, so nothing is ever overwritten.
"""
import os, sys, traceback
import tkinter as tk
from tkinter import ttk, messagebox
import config, sab, sax
class App:
def __init__(self, root):
self.root = root
root.title("%s %s" % (config.NAME, config.VERSION))
root.minsize(620, 340)
frame = ttk.Frame(root, padding=12)
frame.pack(fill="both", expand=True)
frame.columnconfigure(0, weight=1)
frame.rowconfigure(3, weight=1)
# sedona home
home_box = ttk.LabelFrame(frame, text="Sedona home", padding=(8, 4))
home_box.grid(row=0, column=0, sticky="ew")
home_box.columnconfigure(0, weight=1)
self.home_var = tk.StringVar(value="(not set)")
ttk.Label(home_box, textvariable=self.home_var).grid(row=0, column=0, sticky="w")
ttk.Button(home_box, text="Change...", command=self.change_home).grid(row=0, column=1, padx=(8, 0))
# the two directions
buttons = ttk.Frame(frame, padding=(0, 12))
buttons.grid(row=1, column=0, sticky="ew")
buttons.columnconfigure(0, weight=1)
buttons.columnconfigure(1, weight=1)
self.b_sab2sax = ttk.Button(buttons, text="SAB file to SAX file", padding=10,
command=lambda: self.run("sab"))
self.b_sab2sax.grid(row=0, column=0, sticky="ew", padx=(0, 6))
self.b_sax2sab = ttk.Button(buttons, text="SAX file to SAB file", padding=10,
command=lambda: self.run("sax"))
self.b_sax2sab.grid(row=0, column=1, sticky="ew", padx=(6, 0))
self.status = tk.StringVar(value="Choose a direction.")
ttk.Label(frame, textvariable=self.status).grid(row=2, column=0, sticky="w")
# log
log_box = ttk.Frame(frame)
log_box.grid(row=3, column=0, sticky="nsew", pady=(8, 0))
log_box.columnconfigure(0, weight=1)
log_box.rowconfigure(0, weight=1)
self.log_text = tk.Text(log_box, height=10, wrap="word", state="disabled")
self.log_text.grid(row=0, column=0, sticky="nsew")
bar = ttk.Scrollbar(log_box, orient="vertical", command=self.log_text.yview)
bar.grid(row=0, column=1, sticky="ns")
self.log_text.configure(yscrollcommand=bar.set)
self.home = None
root.after(50, self.first_home)
# ---------- helpers ----------
def log(self, line=""):
self.log_text.configure(state="normal")
self.log_text.insert("end", line + "\n")
self.log_text.see("end")
self.log_text.configure(state="disabled")
self.root.update_idletasks() # so it is on screen before the work starts
def set_home(self, path):
self.home = path
self.home_var.set(path or "(not set)")
def first_home(self):
"""Resolve sedona home at startup; the folder chooser opens if it isn't set."""
try:
self.set_home(config.get_sedona_home())
self.log("sedona home: %s" % self.home)
except SystemExit as e:
self.log("no sedona home: %s" % e)
messagebox.showwarning(config.title("Sedona home"),
"No sedona home selected.\n\n"
"Set it with Change... before converting.")
def change_home(self):
picked = config.ask_directory("Select Sedona home (the folder containing manifests/)",
self.home)
if not picked:
return
picked = os.path.normpath(picked)
if not config.is_sedona_home(picked):
messagebox.showerror(config.title("Sedona home"),
"%s has no manifests/ subfolder." % picked)
return
config.write_prop(config.KEY, picked)
self.set_home(picked)
self.log("sedona home: %s (saved to %s)" % (picked, config.PROPS))
# ---------- conversion ----------
def run(self, direction):
"""direction: 'sab' means sab -> sax, 'sax' means sax -> sab."""
if not config.is_sedona_home(self.home):
messagebox.showerror(config.title("Sedona home"),
"Set a valid sedona home first (Change...).")
return
src = config.choose_sab_input() if direction == "sab" else config.choose_sax_file()
if not src:
self.status.set("Cancelled.")
return
src = os.path.abspath(src)
# show what was chosen, before doing anything with it
self.log()
self.log("%s -> %s" % (direction, "sax" if direction == "sab" else "sab"))
self.log(" INPUT FILE:")
self.log(" %s" % os.path.basename(src))
self.log(" INPUT PATH:")
self.log(" %s" % os.path.dirname(src))
self.status.set("Converting %s ..." % os.path.basename(src))
if direction == "sab":
out = os.path.join(os.path.dirname(src), sax.default_sax_name(src))
work = lambda: sax.decode(src, self.home, out)
else:
out = os.path.join(os.path.dirname(src), sab.default_sab_name(src))
work = lambda: sab.encode(src, self.home, out)
self.busy(True)
try:
n = work()
except Exception as e:
self.busy(False)
self.log(" FAILED: %s" % e)
self.status.set("Failed.")
traceback.print_exc()
messagebox.showerror(config.title("Conversion failed"), str(e))
return
self.busy(False)
self.log(" OUTPUT FILE:")
self.log(" %s" % os.path.basename(out))
self.log(" OUTPUT PATH:")
self.log(" %s" % os.path.dirname(out))
self.log(" wrote %d bytes" % n)
self.status.set("Done: %s" % os.path.basename(out))
def busy(self, on):
state = "disabled" if on else "normal"
self.b_sab2sax.configure(state=state)
self.b_sax2sab.configure(state=state)
self.root.configure(cursor="watch" if on else "")
self.root.update_idletasks()
def main():
root = tk.Tk()
App(root)
root.mainloop()
return 0
if __name__ == "__main__":
sys.exit(main())

22
run.py Normal file
View File

@ -0,0 +1,22 @@
"""Start here.
python run.py the window
pythonw run.py the window, without a console behind it
python run.py app.sab [out] straight to the command line converter
No arguments means the GUI; any argument is passed through to sab2sax.py, so
the same entry point serves a double-click and a script.
"""
import sys
def main(argv):
if len(argv) > 1:
import sab2sax
return sab2sax.main(argv) or 0
import gui
return gui.main()
if __name__ == "__main__":
sys.exit(main(sys.argv))

11
sax.py
View File

@ -225,15 +225,20 @@ def read_app(sab_path, home):
order.append(cid)
# rebuild the tree from the child/sibling id chain
def must(cid, what, owner):
if cid not in by_id:
raise Exception("comp %d %s: missing %s %d" % (owner["id"], owner["name"], what, cid))
return by_id[cid]
for c in by_id.values():
c["parent"] = by_id[c["parentId"]] if c["parentId"] != 0xFFFF else None
c["parent"] = must(c["parentId"], "parent", c) if c["parentId"] != 0xFFFF else None
if c["childId"] != 0xFFFF:
kid = by_id[c["childId"]]
kid = must(c["childId"], "child", c)
while True:
c["kids"].append(kid)
if kid["siblingId"] == 0xFFFF:
break
kid = by_id[kid["siblingId"]]
kid = must(kid["siblingId"], "nextSibling", kid)
links = []
while True:

1266
testprogram/test_normal.sax Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

139
verify.py
View File

@ -1,27 +1,38 @@
"""Regression check: our output must be byte-identical to sedonac.exe's, both ways.
"""Regression checks: our output must be byte-identical to sedonac.exe's, and a
conversion must survive a round trip.
sax -> sab check_encode()
sab -> sax check_decode()
sab -> sax -> sab round trip
sax -> sab check_encode()
sab -> sax check_decode()
sab -> sax -> sab round_trip_sab() bytes must come back identical
sax -> sab -> sax -> sab round_trip_sax() the two sabs must be identical
Usage: python verify.py [sedona_home]
Usage: python verify.py [app.sax|app.sab] [sedona_home]
Without an argument the home comes from system.properties (see config.py), which
pops a folder chooser the first time.
With no file argument a chooser opens; cancel it and the built-in
`testprogram/` regression runs instead (if those files are present). Sedona home
comes from system.properties (see config.py) unless given.
"""
import os, sys, subprocess, tempfile, shutil
import sab, sax, config
HERE = os.path.dirname(os.path.abspath(__file__))
HOME = sys.argv[1] if len(sys.argv) > 1 else config.get_sedona_home()
TESTS = os.path.join(HERE, "testprogram")
HOME = None
def home():
"""Resolved once, on first use -- so importing this module opens no dialog."""
global HOME
if HOME is None:
HOME = config.get_sedona_home()
return HOME
def sedonac(src, tmp):
"""Run sedonac on a copy of `src`; return the file it produced (needs a JRE)."""
work = os.path.join(tmp, os.path.basename(src))
shutil.copy(src, work)
exe = os.path.join(HOME, "bin", "sedonac.exe")
exe = os.path.join(home(), "bin", "sedonac.exe")
r = subprocess.run([exe, work], capture_output=True, text=True)
if r.returncode != 0:
print(" sedonac failed:\n" + r.stdout + r.stderr)
@ -30,30 +41,32 @@ def sedonac(src, tmp):
return os.path.splitext(work)[0] + other
def compare(label, reference, mine, binary):
def compare(label, reference, mine, binary, names=("sedonac", "ours")):
a = open(reference, "rb").read()
b = open(mine, "rb").read()
if a == b:
print(" PASS %-28s %d bytes identical" % (label, len(a)))
print(" PASS %-34s %d bytes identical" % (label, len(a)))
return True
diffs = [i for i in range(min(len(a), len(b))) if a[i] != b[i]]
print(" FAIL %-28s sedonac=%d ours=%d, %d byte diffs" % (label, len(a), len(b), len(diffs)))
print(" FAIL %-34s %s=%d %s=%d, %d byte diffs" % (
label, names[0], len(a), names[1], len(b), len(diffs)))
if diffs:
i = diffs[0]
if binary:
lo, hi = max(0, i - 12), i + 12
print(" first diff at offset %d" % i)
print(" sedonac %s" % a[lo:hi].hex())
print(" ours %s" % b[lo:hi].hex())
print(" %-8s %s" % (names[0], a[lo:hi].hex()))
print(" %-8s %s" % (names[1], b[lo:hi].hex()))
else:
line = a[:i].count(b"\n") + 1
print(" first diff at line %d" % line)
print(" sedonac %s" % a.splitlines()[line - 1])
print(" ours %s" % b.splitlines()[line - 1])
print(" %-8s %s" % (names[0], a.splitlines()[line - 1]))
print(" %-8s %s" % (names[1], b.splitlines()[line - 1]))
return False
# ---------- against sedonac ----------
def check_encode(sax_file, reference=None):
"""sax -> sab, against `reference` or a fresh sedonac run."""
tmp = tempfile.mkdtemp()
@ -62,7 +75,7 @@ def check_encode(sax_file, reference=None):
if reference is None:
return False
mine = os.path.join(tmp, "mine.sab")
sab.encode(sax_file, HOME, mine)
sab.encode(sax_file, home(), mine)
return compare(os.path.basename(sax_file) + " -> sab", reference, mine, True)
finally:
shutil.rmtree(tmp, ignore_errors=True)
@ -76,36 +89,108 @@ def check_decode(sab_file, reference=None):
if reference is None:
return False
mine = os.path.join(tmp, "mine.sax")
sax.decode(sab_file, HOME, mine)
sax.decode(sab_file, home(), mine)
return compare(os.path.basename(sab_file) + " -> sax", reference, mine, False)
finally:
shutil.rmtree(tmp, ignore_errors=True)
def check_round_trip(sab_file):
# ---------- round trips ----------
def round_trip_sab(sab_file):
"""sab -> sax -> sab must come back to the same bytes."""
tmp = tempfile.mkdtemp()
try:
mid = os.path.join(tmp, "mid.sax")
back = os.path.join(tmp, "back.sab")
sax.decode(sab_file, HOME, mid)
sab.encode(mid, HOME, back)
return compare(os.path.basename(sab_file) + " round trip", sab_file, back, True)
sax.decode(sab_file, home(), mid)
sab.encode(mid, home(), back)
return compare(os.path.basename(sab_file) + " sab->sax->sab",
sab_file, back, True, ("original", "ours"))
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
print("sedona home: %s" % HOME)
def round_trip_sax(sax_file):
"""sax -> sab -> sax -> sab: the two sabs must match.
The sax leg is not compared against the original file, because a .sab holds
no runtime prop values and no formatting -- coming back through it is lossy
on purpose. The binary is the thing that has to be stable, so we compare
that. Any difference from the original sax is printed as a note, not a
failure."""
tmp = tempfile.mkdtemp()
try:
first = os.path.join(tmp, "first.sab")
mid = os.path.join(tmp, "mid.sax")
second = os.path.join(tmp, "second.sab")
sab.encode(sax_file, home(), first)
sax.decode(first, home(), mid)
sab.encode(mid, home(), second)
ok = compare(os.path.basename(sax_file) + " sax->sab->sax->sab",
first, second, True, ("1st sab", "2nd sab"))
a = open(sax_file, "rb").read()
b = open(mid, "rb").read()
if a != b:
al, bl = a.splitlines(), b.splitlines()
print(" note: the regenerated sax differs from the original "
"(%d vs %d lines) -- expected where the original carries runtime "
"props or different formatting" % (len(al), len(bl)))
return ok
finally:
shutil.rmtree(tmp, ignore_errors=True)
def check_file(path):
"""Round-trip whatever was chosen, by extension."""
ext = os.path.splitext(path)[1].lower()
if ext not in (".sax", ".sab"):
print(" don't know what to do with %s -- expected .sax or .sab" % path)
return False
try:
return round_trip_sax(path) if ext == ".sax" else round_trip_sab(path)
except Exception as e: # a malformed file is a failure, not a traceback
print(" FAIL %-34s %s" % (os.path.basename(path), e))
return False
def builtin():
"""The checked-in pair, when testprogram/ is populated locally."""
saxf = os.path.join(TESTS, "test_normal.sax")
sabf = os.path.join(TESTS, "test_normal_sedonac.sab")
missing = [f for f in (saxf, sabf) if not os.path.isfile(f)]
if missing:
# testprogram/ is deliberately not in the repo -- put the pair back to run this
print(" SKIP no test files: %s" % ", ".join(os.path.basename(f) for f in missing))
sys.exit(0)
return None
ok = check_encode(saxf, sabf)
ok &= check_decode(sabf, saxf)
ok &= check_round_trip(sabf)
sys.exit(0 if ok else 1)
ok &= round_trip_sab(sabf)
ok &= round_trip_sax(saxf)
return ok
def main(argv):
global HOME
print("%s %s" % (config.NAME, config.VERSION))
path = argv[1] if len(argv) > 1 else config.choose_input_file()
if len(argv) > 2:
HOME = argv[2]
print("sedona home: %s" % home())
if not path:
print("no file chosen -- running the built-in checks")
ok = builtin()
return 0 if ok is not False else 1
path = os.path.abspath(path)
if not os.path.isfile(path):
print("no such file: " + path)
return 1
return 0 if check_file(path) else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))