# HG changeset patch # User Matthew Wild # Date 1679584350 0 # Node ID 662bd8c5ae2855eed3638d0c28be41faa6eceee9 # Parent 14ed4cb241f44797f9ef849eafb5a1f346a20a3b Serialize XML in a consistent order by default This overrides all XML serialization to emit attributes in an ordered form, so the XML will match across multiple runs. This can be useful for comparing different runs, or even two stanzas printed in the same run (e.g. if there is a mismatch). diff -r 14ed4cb241f4 -r 662bd8c5ae28 main.lua --- a/main.lua Thu Mar 23 15:09:10 2023 +0000 +++ b/main.lua Thu Mar 23 15:12:30 2023 +0000 @@ -15,6 +15,7 @@ local action_timeout = 10; local verse_log_levels = { "warn", "error" }; local quiet = false; +local ordered = true; local force_summary = false; local serve_mode = false; local serve_origin = nil; @@ -120,6 +121,8 @@ elseif opt == "--serve-port" then serve_mode = assert(tonumber(get_value()), "expected port number"); serve_origin = assert(get_value(), "origin expected for '--serve-port'"); + elseif opt == "--unordered" then + ordered = false; else error("Unhandled command-line option: "..opt); end @@ -280,10 +283,15 @@ local files = process_options(); local console_handlers = require "scansion.console".new({ + ordered = ordered; summary = not(quiet) or force_summary; quiet = quiet; }); +if ordered then + require "scansion.ordered_serializer".enable(); +end + local function console_logger(event, data) local h = console_handlers[event]; if h then diff -r 14ed4cb241f4 -r 662bd8c5ae28 scansion/console.lua --- a/scansion/console.lua Thu Mar 23 15:09:10 2023 +0000 +++ b/scansion/console.lua Thu Mar 23 15:12:30 2023 +0000 @@ -1,86 +1,90 @@ -local pretty = require "scansion.pretty".new({}); local function lines(l, indent) return table.concat(l, "\n"..string.rep(" ", indent or 0)); end -local handlers = { - ["script"] = function (data) - return "TEST: "..(data.title or data.filename or "untitled"); - end; - ["test-passed"] = function () - return "PASS"; - end; - ["test-failed"] = function (data) - local error_text; - if data.reason and data.reason.type == "unexpected-stanza" then - error_text = "Received unexpected stanza:\n\n"..pretty(data.reason.data.stanza, 4); - if data.reason.data.expected then - error_text = error_text.."\n\nExpected:\n\n"..pretty(data.reason.data.expected, 4); - end - else - error_text = tostring(data.reason); - end - return lines({ - "FAILED: "..data.name; - ""; - (error_text:gsub("\n", "\n ")); - ""; - }, 4); - end; - ["test-error"] = function (data) - return lines({ - "ERROR: "..data.name; - ""; - (tostring(data.reason):gsub("\n", "\n ")); - ""; - }, 4); - end; - - ["action"] = function (data) - local action = data.action; - local obj_type = data.object_type; - local l = {}; - if data.annotation then - table.insert(l, action.annotation); - end - table.insert(l, data.object.." "..action); - if data.extra and obj_type == "client" and (action == "sends" or action == "receives") then - table.insert(l, "\n"..pretty(lines(data.extra), 4).."\n"); - end - return lines(l); - end; - - ["end"] = function (data) - local r = {}; - - local all_results = {}; - for k, v in pairs(data.summary.all) do - table.insert(all_results, v); - end - table.sort(all_results, function (a, b) return a.name < b.name end); - - print(""); - print("Summary"); - print("-------"); - - for _, test_result in ipairs(all_results) do - print("", test_result.status, test_result.name); - end - - print(""); - - for _, test_result in ipairs{ "ok", "fail", "error", "skipped", "total" } do - local count = data.summary[test_result] and #data.summary[test_result] or 0; - table.insert(r, tostring(count).." "..test_result); - end - return table.concat(r, " / "); - end; -}; - local quiet_handlers = { "test-failed", "test-error" }; local function new(config) + local pretty = require "scansion.pretty".new({ + sorted = config.ordered; + }); + + local handlers = { + ["script"] = function (data) + return "TEST: "..(data.title or data.filename or "untitled"); + end; + ["test-passed"] = function () + return "PASS"; + end; + ["test-failed"] = function (data) + local error_text; + if data.reason and data.reason.type == "unexpected-stanza" then + error_text = "Received unexpected stanza:\n\n"..pretty(data.reason.data.stanza, 4); + if data.reason.data.expected then + error_text = error_text.."\n\nExpected:\n\n"..pretty(data.reason.data.expected, 4); + end + else + error_text = tostring(data.reason); + end + return lines({ + "FAILED: "..data.name; + ""; + (error_text:gsub("\n", "\n ")); + ""; + }, 4); + end; + ["test-error"] = function (data) + return lines({ + "ERROR: "..data.name; + ""; + (tostring(data.reason):gsub("\n", "\n ")); + ""; + }, 4); + end; + + ["action"] = function (data) + local action = data.action; + local obj_type = data.object_type; + local l = {}; + if data.annotation then + table.insert(l, action.annotation); + end + table.insert(l, data.object.." "..action); + if data.extra and obj_type == "client" and (action == "sends" or action == "receives") then + table.insert(l, "\n"..pretty(lines(data.extra), 4).."\n"); + end + return lines(l); + end; + + ["end"] = function (data) + local r = {}; + + local all_results = {}; + for _, v in pairs(data.summary.all) do + table.insert(all_results, v); + end + table.sort(all_results, function (a, b) return a.name < b.name end); + + print(""); + print("Summary"); + print("-------"); + + for _, test_result in ipairs(all_results) do + print("", test_result.status, test_result.name); + end + + print(""); + + for _, test_result in ipairs{ "ok", "fail", "error", "skipped", "total" } do + local count = data.summary[test_result] and #data.summary[test_result] or 0; + table.insert(r, tostring(count).." "..test_result); + end + return table.concat(r, " / "); + end; + }; + + local h = {}; if config.quiet then for _, handler_name in ipairs(quiet_handlers) do diff -r 14ed4cb241f4 -r 662bd8c5ae28 scansion/ordered_serializer.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scansion/ordered_serializer.lua Thu Mar 23 15:12:30 2023 +0000 @@ -0,0 +1,56 @@ +local s_find, s_gsub, s_match = string.find, string.gsub, string.match; +local t_concat, t_insert = table.concat, table.insert; + +local pairs = require "util.iterators".sorted_pairs; + +local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; +local function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end + +local function _dostring(t, buf, self, _xml_escape, parentns) + local nsid = 0; + local name = t.name + t_insert(buf, "<"..name); + for k, v in pairs(t.attr) do + if s_find(k, "\1", 1, true) then + local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$"); + nsid = nsid + 1; + t_insert(buf, " xmlns:ns"..nsid.."='".._xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='".._xml_escape(v).."'"); + elseif not(k == "xmlns" and v == parentns) then + t_insert(buf, " "..k.."='".._xml_escape(v).."'"); + end + end + local len = #t; + if len == 0 then + t_insert(buf, "/>"); + else + t_insert(buf, ">"); + for n=1,len do + local child = t[n]; + if child.name then + self(child, buf, self, _xml_escape, t.attr.xmlns); + else + t_insert(buf, _xml_escape(child)); + end + end + t_insert(buf, ""); + end +end + +return { + enable = function () + local stanza_mt = require "util.stanza".stanza_mt; + stanza_mt._unsorted_tostring = stanza_mt.__tostring; + function stanza_mt.__tostring(t) + local buf = {}; + _dostring(t, buf, _dostring, xml_escape, nil); + return t_concat(buf); + end; + end; + disable = function () + local stanza_mt = require "util.stanza".stanza_mt; + if stanza_mt._unsorted_tostring then + stanza_mt.__tostring = stanza_mt._unsorted_tostring; + end + end; +}; + diff -r 14ed4cb241f4 -r 662bd8c5ae28 scansion/pretty.lua --- a/scansion/pretty.lua Thu Mar 23 15:09:10 2023 +0000 +++ b/scansion/pretty.lua Thu Mar 23 15:12:30 2023 +0000 @@ -2,6 +2,9 @@ local xml = require "scansion.xml"; local s_format, s_gsub = string.format, string.gsub; +local unsorted_pairs = pairs; +local sorted_pairs = require "util.iterators".sorted_pairs; + local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; local function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end @@ -18,6 +21,7 @@ local default_config = { indent = 2; preserve_whitespace = false; + sorted = true; }; local function new(user_config) @@ -27,6 +31,8 @@ local style_tagname = getstyle("red"); local style_punc = getstyle("magenta"); + local pairs = user_config.sorted and sorted_pairs or unsorted_pairs; + local attr_format = " "..getstring(style_attrk, "%s")..getstring(style_punc, "=")..getstring(style_attrv, "'%s'"); local open_tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">"); local close_tag_format = getstring(style_punc, ""); diff -r 14ed4cb241f4 -r 662bd8c5ae28 squishy --- a/squishy Thu Mar 23 15:09:10 2023 +0000 +++ b/squishy Thu Mar 23 15:12:30 2023 +0000 @@ -14,6 +14,7 @@ Module "scansion.xml" Module "scansion.console" Module "scansion.pretty" +Module "scansion.ordered_serializer" if GetOption "with-verse" then Module "verse"