-- Unit checks for custom/extensions/syntax-highlight.lua, run under a -- standalone Lua by t0502-syntax-highlight.sh with the script path as the -- first argument. The filter remembers whether the lexers loaded for the -- life of the interpreter, so the two sides of that split are two runs of -- this file. In highlight mode the shell points CGIT_SCINTILLUA_PATH at the -- fake-lexers directory beside this file and every path through a working -- lexer is checked. In nolexers mode it points at an empty directory, so -- the escaped fallback is what everything renders through, and the run ends -- with every directory the filter probes hidden, for a server with no -- Scintillua at all. local test_directory = arg[0]:match("^(.*)/") or "." local h = dofile(test_directory .. "/harness.lua") local script = arg[1] local mode = h.mode("highlight", "nolexers") h.load(script) local function render(filename, text) return h.run({ filename }, { text }) end if mode == "nolexers" then local out, ret = render("hello.fake", "if 42") h.equals("without lexers the source passes through uncoloured", out, "if 42") h.equals("close answers zero", ret, 0) h.equals("and the error log says the lexers would not load", h.error_log(), "[cgit] Unable to load the Scintillua lexers: module 'lexer' not found:\n") out = render("hello.fake", "a & c") h.equals("the fallback escapes markup", out, "a <b> & c") h.equals("and the error log hears of it only once", h.error_log(), "") out = render("hello.fake", "a\0b") h.equals("a nul byte cuts the escaped blob short", out, "a") -- Large output leaves in slices, and a byte lost at a boundary would -- put the line numbers beside it out of step, so the whole escaped -- blob has to come back intact across several slices. out = render("big.fake", string.rep("x", 30000)) h.equals("slicing loses nothing at the boundaries", out, string.rep("x<y>", 30000)) out = render("hello.fake", "a\rb\vc\fd\r\ne") h.equals("the fallback escapes what a browser breaks at", out, "a\\rb\\vc" .. "\\fd\ne") -- The line separator starts on the last byte of the first slice, and -- its other two bytes would land in the next one. out = render("big.fake", string.rep("x", 64 * 1024 - 1) .. "\226\128\168y") h.equals("a separator across a slice boundary is still escaped", out, string.rep("x", 64 * 1024 - 1) .. "\\u2028y") -- With the variable unset and no lexers beside cgitrc or in any directory -- the filter probes, it finds nothing to load, the usual case on a server -- without Scintillua. local file = assert(io.open(script, "rb")) local source = file:read("*a") file:close() for dir in source:match("local scintillua_dirs = (%b{})"):gmatch('"([^"]+)"') do h.redirect_file(dir .. "/lexer.lua", "/nonexistent/lexer.lua") end h.setenv("CGIT_SCINTILLUA_PATH", nil) h.setenv("CGIT_CONFIG", nil) h.load(script) out = render("hello.fake", "a ") h.equals("with no lexers found the source is served escaped", out, "a <b>") h.equals("and the error log says none were found", h.error_log(), "[cgit] Unable to find the Scintillua lexers, so source is shown uncoloured\n") -- The filter takes a module called lexer only when it is Scintillua, so -- one that is something else is never used. A Scintillua older than 6.0 -- has no detect() and colours nothing, and lexers that need a missing -- lpeg say why in the log. Each loads the filter afresh with the module -- it finds forced through package.preload. local function load_with_lexer(loader) package.loaded.lexer = nil package.preload.lexer = loader h.setenv("CGIT_SCINTILLUA_PATH", "/nonexistent") h.load(script) end local used = false load_with_lexer(function() return { detect = function() used = true end } end) out = render("hello.fake", "a ") h.equals("a lexer module without load is passed over", out, "a <b>") h.check("and never used", not used) h.equals("and the error log stays empty", h.error_log(), "") load_with_lexer(function() return { load = function() return { lex = function() return { "keyword", 2 } end } end } end) out = render("hello.fake", "a ") h.equals("a Scintillua without detect colours nothing", out, "a <b>") h.equals("and logs nothing", h.error_log(), "") h.preload_failure("lpeg") load_with_lexer(function() return require("lpeg") end) out = render("hello.fake", "a ") h.equals("lexers that cannot load lpeg leave the source uncoloured", out, "a <b>") local log = h.error_log() local prefix = "[cgit] Unable to load the Scintillua lexers: " h.equals("and the error log says why", log:sub(1, #prefix), prefix) h.contains("naming lpeg", log, "lpeg deliberately unavailable") h.equals("on one line", log:find("\n", 1, true), #log) h.finish() end local out, ret = render("hello.fake", "if 42 'hi' & zz") h.equals("loaded lexers leave the error log empty", h.error_log(), "") h.contains("keywords are wrapped in their class", out, "if") h.contains("numbers are wrapped in their class", out, "42") h.contains("a dotted tag maps by its first component", out, "'hi'") h.contains("bytes between tokens are escaped", out, " & ") h.contains("later tokens still highlight", out, "zz") h.equals("close answers zero", ret, 0) out = render("hello.fake", "'&'") h.equals("markup inside a token is escaped", out, "'<b>&'") out = render("hello.fake", "-- note\nString x") h.equals("comments and types take their classes", out, "-- note\nString x") out = render("hello.fake", "$color :hover") h.contains("a css property takes the number class", out, "$color") h.contains("a css pseudo-class takes the function class", out, ":hover") out = h.run({ "split.fake" }, { "i", "f" }) h.equals("writes are joined before lexing", out, "if") -- The short lexer tags four bytes and stops, and every byte after it still -- has to reach the page. out = render("cut.short", "abcd") h.equals("a lexer stopping early leaves an escaped tail", out, "abcd<ef>") out = render("boom.badlex", "some ") h.equals("a lexer that raises falls back to escaped text", out, "some <code>") out = render("empty.nothing", "some ") h.equals("a lexer that returns no tokens falls back to escaped text", out, "some <code>") out = render("HELLO.FAKE", "if 42") h.contains("an extension in capitals finds its lexer", out, "if") out = render("post-receive", "#!/usr/bin/fake\nif") h.contains("a shebang picks the lexer of a file with no extension", out, "if") out = render("tool.py", "import ") h.equals("an extension with no lexer serves escaped text", out, "import <x>") out = render("noextension", "words ") h.equals("a file with no extension serves escaped text", out, "words <here>") -- The line numbers beside the code count line feeds alone, so each character -- a browser would start a line at is an escape, and CR LF reaches the page as -- the line feed a browser would have made of it. out = render("hello.fake", "if\r42\vzz\f'a'\r\nif") h.equals("what a browser breaks at is escaped between tokens", out, "if\\r" .. "42\\v" .. "zz\\f" .. "'a'\nif") -- The fake lexer hands back every byte of a separator as a token of its own. out = render("hello.fake", "a\194\133b\226\128\168c\226\128\169d") h.equals("a separator split between tokens is escaped whole", out, "a\\u0085" .. "b\\u2028" .. "c\\u2029" .. "d") out = render("big.fake", string.rep("a", 512 * 1024 + 1)) h.excludes("an oversized blob skips the lexing pass", out, "a") -- cgit's sink stops at the first NUL byte, as the harness does, so a blob -- holding one loses everything from there on. out = render("hello.fake", "a\0b") h.equals("a nul byte cuts the highlighted blob short", out, "a") out, ret = render("empty.fake", "") h.equals("an empty blob renders empty", out, "") h.equals("an empty blob still answers zero", ret, 0) h.finish()