Thu, 23 Mar 2023 15:12:30 +0000
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).
main.lua | file | annotate | diff | comparison | revisions | |
scansion/console.lua | file | annotate | diff | comparison | revisions | |
scansion/ordered_serializer.lua | file | annotate | diff | comparison | revisions | |
scansion/pretty.lua | file | annotate | diff | comparison | revisions | |
squishy | file | annotate | diff | comparison | revisions |
--- 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
--- 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
--- /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, "</"..name..">"); + 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; +}; +
--- 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, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");