Compare commits
No commits in common. "main" and "v0.0.0.001" have entirely different histories.
main
...
v0.0.0.001
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
testprogram/
|
||||||
*-sab2sax-*.sab
|
*-sab2sax-*.sab
|
||||||
*-sab2sax-*.sax
|
*-sab2sax-*.sax
|
||||||
|
|||||||
81
README.md
81
README.md
@ -10,46 +10,13 @@ runtime lib).
|
|||||||
Verified byte-identical to `sedonac.exe` in both directions on a 189-component / 217-link app across
|
Verified byte-identical to `sedonac.exe` in both directions on a 189-component / 217-link app across
|
||||||
18 kits.
|
18 kits.
|
||||||
|
|
||||||
## Run it
|
## Usage
|
||||||
|
|
||||||
python run.py the window
|
python sab2sax.py [in.sax|in.sab] [out]
|
||||||
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`,
|
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 — `sab2sax.py` on its own asks for
|
feed it a `.sab` and you get a `.sax`. Both arguments are optional, so you can run it by
|
||||||
what it needs:
|
double-click:
|
||||||
|
|
||||||
* no input → a file chooser opens, listing `.sax` and `.sab`;
|
* 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:
|
* no output → a save dialog opens next to the input, pre-filled with a timestamped name:
|
||||||
@ -96,56 +63,36 @@ set the key by hand instead.
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
`config.NAME` / `config.VERSION` — currently `SAB - SAX Sedona Files Converter 0.0.0.002`. The window
|
`config.NAME` / `config.VERSION` — currently `sab2sax 0.0.0.001`. Every dialog title bar reads
|
||||||
title and every dialog title bar carry it; a chooser reads `SAB - SAX Sedona Files Converter
|
`sab2sax 0.0.0.001 - Select the .sax or .sab application file to convert`, and the CLI prints the
|
||||||
0.0.0.002 - Select the .sax or .sab application file to convert`, and the CLI prints the same line
|
same line when it starts, so it is obvious which build produced a file. Bump it in
|
||||||
when it starts, so it is obvious which build produced a file. Bump it in [config.py](config.py) and
|
[config.py](config.py) and both follow.
|
||||||
all of them follow.
|
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
python verify.py [app.sax|app.sab] [sedona_home]
|
python verify.py [sedona_home]
|
||||||
|
|
||||||
Point it at any app of yours and it round-trips it, direction by extension:
|
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
|
||||||
app.sab -> sab -> sax -> sab must come back byte-identical
|
repo (gitignored) — drop the pair in yourself, otherwise verify prints `SKIP` and exits 0:
|
||||||
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.sax -> sab vs test_normal_sedonac.sab
|
||||||
test_normal_sedonac.sab -> sax vs test_normal.sax
|
test_normal_sedonac.sab -> sax vs test_normal.sax
|
||||||
both round trips as above
|
test_normal_sedonac.sab -> sax -> sab back to the same bytes
|
||||||
|
|
||||||
`check_encode(sax)` / `check_decode(sab)` with no reference argument run `bin/sedonac.exe` on a copy
|
`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
|
of the input and diff against that instead — a live comparison, if you have a JRE it will start
|
||||||
under.
|
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
|
## 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
|
sab2sax.py the CLI: dispatches on the input extension
|
||||||
sab.py sax -> sab encoder
|
sab.py sax -> sab encoder
|
||||||
sax.py sab -> sax decoder
|
sax.py sab -> sax decoder
|
||||||
config.py system.properties read/write, folder + file choosers
|
config.py system.properties read/write, folder + file choosers
|
||||||
verify.py regression check against sedonac's output, both directions
|
verify.py regression check against sedonac's output, both directions
|
||||||
system.properties sedona.home
|
system.properties sedona.home
|
||||||
testprogram/ test apps verify.py runs against
|
testprogram/ local only, gitignored: test apps to run verify.py 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
|
## Format
|
||||||
|
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
# 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.
|
|
||||||
14
config.py
14
config.py
@ -14,8 +14,8 @@ chooser is shown and the answer is written back to `system.properties`.
|
|||||||
"""
|
"""
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|
||||||
NAME = "SAB - SAX Sedona Files Converter"
|
NAME = "sab2sax"
|
||||||
VERSION = "0.0.0.002"
|
VERSION = "0.0.0.001"
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
PROPS = os.path.join(HERE, "system.properties")
|
PROPS = os.path.join(HERE, "system.properties")
|
||||||
@ -169,15 +169,7 @@ def choose_input_file(initial=None):
|
|||||||
|
|
||||||
|
|
||||||
def choose_sax_file(initial=None):
|
def choose_sax_file(initial=None):
|
||||||
"""Open a .sax -- the input of a sax -> sab conversion."""
|
return ask_open_file("Select the .sax application file", [SAX, ANY], initial)
|
||||||
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):
|
def choose_sab_file(initialfile=None, initial=None):
|
||||||
|
|||||||
166
gui.py
166
gui.py
@ -1,166 +0,0 @@
|
|||||||
"""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
22
run.py
@ -1,22 +0,0 @@
|
|||||||
"""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
11
sax.py
@ -225,20 +225,15 @@ def read_app(sab_path, home):
|
|||||||
order.append(cid)
|
order.append(cid)
|
||||||
|
|
||||||
# rebuild the tree from the child/sibling id chain
|
# 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():
|
for c in by_id.values():
|
||||||
c["parent"] = must(c["parentId"], "parent", c) if c["parentId"] != 0xFFFF else None
|
c["parent"] = by_id[c["parentId"]] if c["parentId"] != 0xFFFF else None
|
||||||
if c["childId"] != 0xFFFF:
|
if c["childId"] != 0xFFFF:
|
||||||
kid = must(c["childId"], "child", c)
|
kid = by_id[c["childId"]]
|
||||||
while True:
|
while True:
|
||||||
c["kids"].append(kid)
|
c["kids"].append(kid)
|
||||||
if kid["siblingId"] == 0xFFFF:
|
if kid["siblingId"] == 0xFFFF:
|
||||||
break
|
break
|
||||||
kid = must(kid["siblingId"], "nextSibling", kid)
|
kid = by_id[kid["siblingId"]]
|
||||||
|
|
||||||
links = []
|
links = []
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
135
verify.py
135
verify.py
@ -1,38 +1,27 @@
|
|||||||
"""Regression checks: our output must be byte-identical to sedonac.exe's, and a
|
"""Regression check: our output must be byte-identical to sedonac.exe's, both ways.
|
||||||
conversion must survive a round trip.
|
|
||||||
|
|
||||||
sax -> sab check_encode()
|
sax -> sab check_encode()
|
||||||
sab -> sax check_decode()
|
sab -> sax check_decode()
|
||||||
sab -> sax -> sab round_trip_sab() bytes must come back identical
|
sab -> sax -> sab round trip
|
||||||
sax -> sab -> sax -> sab round_trip_sax() the two sabs must be identical
|
|
||||||
|
|
||||||
Usage: python verify.py [app.sax|app.sab] [sedona_home]
|
Usage: python verify.py [sedona_home]
|
||||||
|
|
||||||
With no file argument a chooser opens; cancel it and the built-in
|
Without an argument the home comes from system.properties (see config.py), which
|
||||||
`testprogram/` regression runs instead (if those files are present). Sedona home
|
pops a folder chooser the first time.
|
||||||
comes from system.properties (see config.py) unless given.
|
|
||||||
"""
|
"""
|
||||||
import os, sys, subprocess, tempfile, shutil
|
import os, sys, subprocess, tempfile, shutil
|
||||||
import sab, sax, config
|
import sab, sax, config
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
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")
|
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):
|
def sedonac(src, tmp):
|
||||||
"""Run sedonac on a copy of `src`; return the file it produced (needs a JRE)."""
|
"""Run sedonac on a copy of `src`; return the file it produced (needs a JRE)."""
|
||||||
work = os.path.join(tmp, os.path.basename(src))
|
work = os.path.join(tmp, os.path.basename(src))
|
||||||
shutil.copy(src, work)
|
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)
|
r = subprocess.run([exe, work], capture_output=True, text=True)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
print(" sedonac failed:\n" + r.stdout + r.stderr)
|
print(" sedonac failed:\n" + r.stdout + r.stderr)
|
||||||
@ -41,32 +30,30 @@ def sedonac(src, tmp):
|
|||||||
return os.path.splitext(work)[0] + other
|
return os.path.splitext(work)[0] + other
|
||||||
|
|
||||||
|
|
||||||
def compare(label, reference, mine, binary, names=("sedonac", "ours")):
|
def compare(label, reference, mine, binary):
|
||||||
a = open(reference, "rb").read()
|
a = open(reference, "rb").read()
|
||||||
b = open(mine, "rb").read()
|
b = open(mine, "rb").read()
|
||||||
if a == b:
|
if a == b:
|
||||||
print(" PASS %-34s %d bytes identical" % (label, len(a)))
|
print(" PASS %-28s %d bytes identical" % (label, len(a)))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
diffs = [i for i in range(min(len(a), len(b))) if a[i] != b[i]]
|
diffs = [i for i in range(min(len(a), len(b))) if a[i] != b[i]]
|
||||||
print(" FAIL %-34s %s=%d %s=%d, %d byte diffs" % (
|
print(" FAIL %-28s sedonac=%d ours=%d, %d byte diffs" % (label, len(a), len(b), len(diffs)))
|
||||||
label, names[0], len(a), names[1], len(b), len(diffs)))
|
|
||||||
if diffs:
|
if diffs:
|
||||||
i = diffs[0]
|
i = diffs[0]
|
||||||
if binary:
|
if binary:
|
||||||
lo, hi = max(0, i - 12), i + 12
|
lo, hi = max(0, i - 12), i + 12
|
||||||
print(" first diff at offset %d" % i)
|
print(" first diff at offset %d" % i)
|
||||||
print(" %-8s %s" % (names[0], a[lo:hi].hex()))
|
print(" sedonac %s" % a[lo:hi].hex())
|
||||||
print(" %-8s %s" % (names[1], b[lo:hi].hex()))
|
print(" ours %s" % b[lo:hi].hex())
|
||||||
else:
|
else:
|
||||||
line = a[:i].count(b"\n") + 1
|
line = a[:i].count(b"\n") + 1
|
||||||
print(" first diff at line %d" % line)
|
print(" first diff at line %d" % line)
|
||||||
print(" %-8s %s" % (names[0], a.splitlines()[line - 1]))
|
print(" sedonac %s" % a.splitlines()[line - 1])
|
||||||
print(" %-8s %s" % (names[1], b.splitlines()[line - 1]))
|
print(" ours %s" % b.splitlines()[line - 1])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ---------- against sedonac ----------
|
|
||||||
def check_encode(sax_file, reference=None):
|
def check_encode(sax_file, reference=None):
|
||||||
"""sax -> sab, against `reference` or a fresh sedonac run."""
|
"""sax -> sab, against `reference` or a fresh sedonac run."""
|
||||||
tmp = tempfile.mkdtemp()
|
tmp = tempfile.mkdtemp()
|
||||||
@ -75,7 +62,7 @@ def check_encode(sax_file, reference=None):
|
|||||||
if reference is None:
|
if reference is None:
|
||||||
return False
|
return False
|
||||||
mine = os.path.join(tmp, "mine.sab")
|
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)
|
return compare(os.path.basename(sax_file) + " -> sab", reference, mine, True)
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
@ -89,108 +76,36 @@ def check_decode(sab_file, reference=None):
|
|||||||
if reference is None:
|
if reference is None:
|
||||||
return False
|
return False
|
||||||
mine = os.path.join(tmp, "mine.sax")
|
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)
|
return compare(os.path.basename(sab_file) + " -> sax", reference, mine, False)
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
# ---------- round trips ----------
|
def check_round_trip(sab_file):
|
||||||
def round_trip_sab(sab_file):
|
|
||||||
"""sab -> sax -> sab must come back to the same bytes."""
|
"""sab -> sax -> sab must come back to the same bytes."""
|
||||||
tmp = tempfile.mkdtemp()
|
tmp = tempfile.mkdtemp()
|
||||||
try:
|
try:
|
||||||
mid = os.path.join(tmp, "mid.sax")
|
mid = os.path.join(tmp, "mid.sax")
|
||||||
back = os.path.join(tmp, "back.sab")
|
back = os.path.join(tmp, "back.sab")
|
||||||
sax.decode(sab_file, home(), mid)
|
sax.decode(sab_file, HOME, mid)
|
||||||
sab.encode(mid, home(), back)
|
sab.encode(mid, HOME, back)
|
||||||
return compare(os.path.basename(sab_file) + " sab->sax->sab",
|
return compare(os.path.basename(sab_file) + " round trip", sab_file, back, True)
|
||||||
sab_file, back, True, ("original", "ours"))
|
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def round_trip_sax(sax_file):
|
if __name__ == "__main__":
|
||||||
"""sax -> sab -> sax -> sab: the two sabs must match.
|
print("sedona home: %s" % HOME)
|
||||||
|
|
||||||
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")
|
saxf = os.path.join(TESTS, "test_normal.sax")
|
||||||
sabf = os.path.join(TESTS, "test_normal_sedonac.sab")
|
sabf = os.path.join(TESTS, "test_normal_sedonac.sab")
|
||||||
missing = [f for f in (saxf, sabf) if not os.path.isfile(f)]
|
missing = [f for f in (saxf, sabf) if not os.path.isfile(f)]
|
||||||
if missing:
|
if missing:
|
||||||
# testprogram/ is deliberately not in the repo -- put the pair back to run this
|
# 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))
|
print(" SKIP no test files: %s" % ", ".join(os.path.basename(f) for f in missing))
|
||||||
return None
|
sys.exit(0)
|
||||||
|
|
||||||
ok = check_encode(saxf, sabf)
|
ok = check_encode(saxf, sabf)
|
||||||
ok &= check_decode(sabf, saxf)
|
ok &= check_decode(sabf, saxf)
|
||||||
ok &= round_trip_sab(sabf)
|
ok &= check_round_trip(sabf)
|
||||||
ok &= round_trip_sax(saxf)
|
sys.exit(0 if ok else 1)
|
||||||
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))
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user