From 6f1dcd8c1cdde8790fd47be9e17cf67c3fe08f69 Mon Sep 17 00:00:00 2001 From: Paul Liverman III Date: Tue, 10 Oct 2017 00:16:09 -0700 Subject: [PATCH] Squashed 'src/lib/inspect/' content from commit a384174 git-subtree-dir: src/lib/inspect git-subtree-split: a384174649e8429cc3270a46cfacc37acaf6e042 --- .travis.yml | 36 +++ CHANGELOG.md | 35 +++ MIT-LICENSE.txt | 20 ++ README.md | 253 ++++++++++++++++++ inspect.lua | 341 ++++++++++++++++++++++++ rockspecs/inspect-1.2-2.rockspec | 23 ++ rockspecs/inspect-2.0-1.rockspec | 23 ++ rockspecs/inspect-3.0-1.rockspec | 23 ++ rockspecs/inspect-3.0-2.rockspec | 23 ++ rockspecs/inspect-3.0-3.rockspec | 23 ++ rockspecs/inspect-3.0-4.rockspec | 23 ++ spec/inspect_spec.lua | 439 +++++++++++++++++++++++++++++++ spec/unindent.lua | 39 +++ 13 files changed, 1301 insertions(+) create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 MIT-LICENSE.txt create mode 100644 README.md create mode 100644 inspect.lua create mode 100644 rockspecs/inspect-1.2-2.rockspec create mode 100644 rockspecs/inspect-2.0-1.rockspec create mode 100644 rockspecs/inspect-3.0-1.rockspec create mode 100644 rockspecs/inspect-3.0-2.rockspec create mode 100644 rockspecs/inspect-3.0-3.rockspec create mode 100644 rockspecs/inspect-3.0-4.rockspec create mode 100644 spec/inspect_spec.lua create mode 100644 spec/unindent.lua diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..91f7f93 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,36 @@ +language: python +sudo: false + +env: + - LUA="lua=5.1" + - LUA="lua=5.2" + - LUA="lua=5.3" + - LUA="luajit=2.0" + - LUA="luajit=2.1" + +before_install: + - pip install hererocks + - hererocks lua_install -r^ --$LUA + - export PATH=$PATH:$PWD/lua_install/bin # Add directory with all installed binaries to PATH + +install: + - luarocks install luacheck + - luarocks install busted + - luarocks install luacov + - luarocks install luacov-coveralls + +script: + - luacheck --std max+busted *.lua spec + - busted --verbose --coverage + +after_success: + - luacov-coveralls --exclude $TRAVIS_BUILD_DIR/lua_install + +branches: + except: + - gh-pages + +notifications: + email: + on_success: change + on_failure: always diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4081219 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +## v3.1.0 + +* Fixes bug: all control codes are escaped correctly (instead of only the named ones such as \n). + Example: \1 becomes \\1 (or \\001 when followed by a digit) +* Fixes bug when using the `process` option in recursive tables +* Overriding global `tostring` with inspect no longer results in an error. +* Simplifies id generation, using less tables and metatables. + +## v3.0.3 +* Fixes a bug which sometimes displayed struct-like parts of tables as sequence-like due + to the way rawlen/the # operator are implemented. + +## v3.0.2 +* Fixes a bug when a table was garbage-collected while inspect was trying to render it + +## v3.0.1 +* Fixes a bug when dealing with tables which have a __len metamethod in Lua >= 5.2 + +## v3.0.0 + +The basic functionality remains as before, but there's one backwards-incompatible change if you used `options.filter`. + +* **Removed** `options.filter` +* **Added** `options.process`, which can be used to do the same as `options.filter`, and more. +* **Added** two new constants, `inspect.METATABLE` and `inspect.KEY` +* **Added** `options.indent` & `options.newline`. + + +## v2.0.0 + +* Ability to deal with LuaJit's custom types +* License change from BSD to MIT +* Moved second parameter (depth) to options (options.depth) +* Added a new parameter, options.filter. +* Reimplemented some parts of the system without object orientation diff --git a/MIT-LICENSE.txt b/MIT-LICENSE.txt new file mode 100644 index 0000000..555835c --- /dev/null +++ b/MIT-LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2013 Enrique García Cota + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9c6f86 --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +inspect.lua +=========== + +[![Build Status](https://travis-ci.org/kikito/inspect.lua.png?branch=master)](https://travis-ci.org/kikito/inspect.lua) +[![Coverage Status](https://coveralls.io/repos/github/kikito/inspect.lua/badge.svg?branch=master)](https://coveralls.io/github/kikito/inspect.lua?branch=master) + + +This library transforms any Lua value into a human-readable representation. It is especially useful for debugging errors in tables. + +The objective here is human understanding (i.e. for debugging), not serialization or compactness. + +Examples of use +=============== + +`inspect` has the following declaration: `local str = inspect(value, )`. + +`value` can be any Lua value. + +`inspect` transforms simple types (like strings or numbers) into strings. + +```lua +assert(inspect(1) == "1") +assert(inspect("Hello") == '"Hello"') +``` + +Tables, on the other hand, are rendered in a way a human can read easily. + +"Array-like" tables are rendered horizontally: + +```lua +assert(inspect({1,2,3,4}) == "{ 1, 2, 3, 4 }") +``` + +"Dictionary-like" tables are rendered with one element per line: + +```lua +assert(inspect({a=1,b=2}) == [[{ + a = 1, + b = 2 +}]]) +``` + +The keys will be sorted alphanumerically when possible. + +"Hybrid" tables will have the array part on the first line, and the dictionary part just below them: + +```lua +assert(inspect({1,2,3,b=2,a=1}) == [[{ 1, 2, 3, + a = 1, + b = 2 +}]]) +``` + +Subtables are indented with two spaces per level. + +```lua +assert(inspect({a={b=2}}) == [[{ + a = { + b = 2 + } +}]]) +``` + +Functions, userdata and any other custom types from Luajit are simply as ``, ``, etc.: + +```lua +assert(inspect({ f = print, ud = some_user_data, thread = a_thread} ) == [[{ + f = , + u = , + thread = +}]]) +``` + +If the table has a metatable, inspect will include it at the end, in a special field called ``: + +```lua +assert(inspect(setmetatable({a=1}, {b=2}) == [[{ + a = 1 + = { + b = 2 + } +}]])) +``` + +`inspect` can handle tables with loops inside them. It will print `` right before the table is printed out the first time, and replace the whole table with `` from then on, preventing infinite loops. + +```lua +local a = {1, 2} +local b = {3, 4, a} +a[3] = b -- a references b, and b references a +assert(inspect(a) == "<1>{ 1, 2, { 3, 4,
} }") +``` + +Notice that since both `a` appears more than once in the expression, it is prefixed by `<1>` and replaced by `
` every time it appears later on. + +### options + +`inspect` has a second parameter, called `options`. It is not mandatory, but when it is provided, it must be a table. + +#### options.depth + +`options.depth` sets the maximum depth that will be printed out. +When the max depth is reached, `inspect` will stop parsing tables and just return `{...}`: + +```lua + +local t5 = {a = {b = {c = {d = {e = 5}}}}} + +assert(inspect(t5, {depth = 4}) == [[{ + a = { + b = { + c = { + d = {...} + } + } + } +}]]) + +assert(inspect(t5, {depth = 2}) == [[{ + a = { + b = {...} + } +}]]) + +``` + +`options.depth` defaults to infinite (`math.huge`). + +#### options.newline & options.indent + +These are the strings used by `inspect` to respectively add a newline and indent each level of a table. + +By default, `options.newline` is `"\n"` and `options.indent` is `" "` (two spaces). + +``` lua +local t = {a={b=1}} + +assert(inspect(t) == [[{ + a = { + b = 1 + } +}]]) + +assert(inspect(t, {newline='@', indent="++"}), "{@++a = {@++++b = 1@++}@}" +``` + +#### options.process + +`options.process` is a function which allow altering the passed object before transforming it into a string. +A typical way to use it would be to remove certain values so that they don't appear at all. + +`options.process` has the following signature: + +``` lua +local processed_item = function(item, path) +``` + +* `item` is either a key or a value on the table, or any of its subtables +* `path` is an array-like table built with all the keys that have been used to reach `item`, from the root. + * For values, it is just a regular list of keys. For example, to reach the 1 in `{a = {b = 1}}`, the `path` + will be `{'a', 'b'}` + * For keys, the special value `inspect.KEY` is inserted. For example, to reach the `c` in `{a = {b = {c = 1}}}`, + the path will be `{'a', 'b', 'c', inspect.KEY }` + * For metatables, the special value `inspect.METATABLE` is inserted. For `{a = {b = 1}}}`, the path + `{'a', {b = 1}, inspect.METATABLE}` means "the metatable of the table `{b = 1}`". +* `processed_item` is the value returned by `options.process`. If it is equal to `item`, then the inspected + table will look unchanged. If it is different, then the table will look different; most notably, if it's `nil`, + the item will dissapear on the inspected table. + +#### Examples + +Remove a particular metatable from the result: + +``` lua +local t = {1,2,3} +local mt = {b = 2} +setmetatable(t, mt) + +local remove_mt = function(item) + if item ~= mt then return item end +end + +-- mt does not appear +assert(inspect(t, {process = remove_mt}) == "{ 1, 2, 3 }") +``` + +The previous exaple only works for a particular metatable. If you want to make *all* metatables, you can use the `path` parameter to check +wether the last element is `inspect.METATABLE`, and return `nil` instead of the item: + +``` lua +local t, mt = ... -- (defined as before) + +local remove_all_metatables = function(item, path) + if path[#path] ~= inspect.METATABLE then return item end +end + +assert(inspect(t, {process = remove_all_metatables}) == "{ 1, 2, 3 }") +``` + +Filter a value: + +```lua +local anonymize_password = function(item, path) + if path[#path] == 'password' then return "XXXX" end + return item +end + +local info = {user = 'peter', password = 'secret'} + +assert(inspect(info, {process = anonymize_password}) == [[{ + password = "XXXX", + user = "peter" +}]]) +``` + +Gotchas / Warnings +================== + +This method is *not* appropriate for saving/restoring tables. It is meant to be used by the programmer mainly while debugging a program. + +Installation +============ + +If you are using luarocks, just run + + luarocks install inspect + +Otherwise, you can just copy the inspect.lua file somewhere in your projects (maybe inside a /lib/ folder) and require it accordingly. + +Remember to store the value returned by require somewhere! (I suggest a local variable named inspect, although others might like table.inspect) + + local inspect = require 'inspect' + -- or -- + local inspect = require 'lib.inspect' + +Also, make sure to read the license; the text of that license file must appear somewhere in your projects' files. For your convenience, it's included at the begining of inspect.lua. + +Specs +===== + +This project uses [busted](http://olivinelabs.com/busted/) for its specs. If you want to run the specs, you will have to install busted first. Then just execute the following from the root inspect folder: + + busted + +Change log +========== + +Read it on the CHANGELOG.md file + + + + + diff --git a/inspect.lua b/inspect.lua new file mode 100644 index 0000000..ae5b430 --- /dev/null +++ b/inspect.lua @@ -0,0 +1,341 @@ +local inspect ={ + _VERSION = 'inspect.lua 3.1.0', + _URL = 'http://github.com/kikito/inspect.lua', + _DESCRIPTION = 'human-readable representations of tables', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2013 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + +local tostring = tostring + +inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) +inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) + +-- Apostrophizes the string if it has quotes, but not aphostrophes +-- Otherwise, it returns a regular quoted string +local function smartQuote(str) + if str:match('"') and not str:match("'") then + return "'" .. str .. "'" + end + return '"' .. str:gsub('"', '\\"') .. '"' +end + +-- \a => '\\a', \0 => '\\0', 31 => '\31' +local shortControlCharEscapes = { + ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", + ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" +} +local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 +for i=0, 31 do + local ch = string.char(i) + if not shortControlCharEscapes[ch] then + shortControlCharEscapes[ch] = "\\"..i + longControlCharEscapes[ch] = string.format("\\%03d", i) + end +end + +local function escape(str) + return (str:gsub("\\", "\\\\") + :gsub("(%c)%f[0-9]", longControlCharEscapes) + :gsub("%c", shortControlCharEscapes)) +end + +local function isIdentifier(str) + return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) +end + +local function isSequenceKey(k, sequenceLength) + return type(k) == 'number' + and 1 <= k + and k <= sequenceLength + and math.floor(k) == k +end + +local defaultTypeOrders = { + ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, + ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 +} + +local function sortKeys(a, b) + local ta, tb = type(a), type(b) + + -- strings and numbers are sorted numerically/alphabetically + if ta == tb and (ta == 'string' or ta == 'number') then return a < b end + + local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] + -- Two default types are compared according to the defaultTypeOrders table + if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] + elseif dta then return true -- default types before custom ones + elseif dtb then return false -- custom types after default ones + end + + -- custom types are sorted out alphabetically + return ta < tb +end + +-- For implementation reasons, the behavior of rawlen & # is "undefined" when +-- tables aren't pure sequences. So we implement our own # operator. +local function getSequenceLength(t) + local len = 1 + local v = rawget(t,len) + while v ~= nil do + len = len + 1 + v = rawget(t,len) + end + return len - 1 +end + +local function getNonSequentialKeys(t) + local keys = {} + local sequenceLength = getSequenceLength(t) + for k,_ in pairs(t) do + if not isSequenceKey(k, sequenceLength) then table.insert(keys, k) end + end + table.sort(keys, sortKeys) + return keys, sequenceLength +end + +local function getToStringResultSafely(t, mt) + local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') + local str, ok + if type(__tostring) == 'function' then + ok, str = pcall(__tostring, t) + str = ok and str or 'error: ' .. tostring(str) + end + if type(str) == 'string' and #str > 0 then return str end +end + +local function countTableAppearances(t, tableAppearances) + tableAppearances = tableAppearances or {} + + if type(t) == 'table' then + if not tableAppearances[t] then + tableAppearances[t] = 1 + for k,v in pairs(t) do + countTableAppearances(k, tableAppearances) + countTableAppearances(v, tableAppearances) + end + countTableAppearances(getmetatable(t), tableAppearances) + else + tableAppearances[t] = tableAppearances[t] + 1 + end + end + + return tableAppearances +end + +local copySequence = function(s) + local copy, len = {}, #s + for i=1, len do copy[i] = s[i] end + return copy, len +end + +local function makePath(path, ...) + local keys = {...} + local newPath, len = copySequence(path) + for i=1, #keys do + newPath[len + i] = keys[i] + end + return newPath +end + +local function processRecursive(process, item, path, visited) + + if item == nil then return nil end + if visited[item] then return visited[item] end + + local processed = process(item, path) + if type(processed) == 'table' then + local processedCopy = {} + visited[item] = processedCopy + local processedKey + + for k,v in pairs(processed) do + processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) + if processedKey ~= nil then + processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) + end + end + + local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) + setmetatable(processedCopy, mt) + processed = processedCopy + end + return processed +end + + + +------------------------------------------------------------------- + +local Inspector = {} +local Inspector_mt = {__index = Inspector} + +function Inspector:puts(...) + local args = {...} + local buffer = self.buffer + local len = #buffer + for i=1, #args do + len = len + 1 + buffer[len] = args[i] + end +end + +function Inspector:down(f) + self.level = self.level + 1 + f() + self.level = self.level - 1 +end + +function Inspector:tabify() + self:puts(self.newline, string.rep(self.indent, self.level)) +end + +function Inspector:alreadyVisited(v) + return self.ids[v] ~= nil +end + +function Inspector:getId(v) + local id = self.ids[v] + if not id then + local tv = type(v) + id = (self.maxIds[tv] or 0) + 1 + self.maxIds[tv] = id + self.ids[v] = id + end + return tostring(id) +end + +function Inspector:putKey(k) + if isIdentifier(k) then return self:puts(k) end + self:puts("[") + self:putValue(k) + self:puts("]") +end + +function Inspector:putTable(t) + if t == inspect.KEY or t == inspect.METATABLE then + self:puts(tostring(t)) + elseif self:alreadyVisited(t) then + self:puts('
') + elseif self.level >= self.depth then + self:puts('{...}') + else + if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end + + local nonSequentialKeys, sequenceLength = getNonSequentialKeys(t) + local mt = getmetatable(t) + local toStringResult = getToStringResultSafely(t, mt) + + self:puts('{') + self:down(function() + if toStringResult then + self:puts(' -- ', escape(toStringResult)) + if sequenceLength >= 1 then self:tabify() end + end + + local count = 0 + for i=1, sequenceLength do + if count > 0 then self:puts(',') end + self:puts(' ') + self:putValue(t[i]) + count = count + 1 + end + + for _,k in ipairs(nonSequentialKeys) do + if count > 0 then self:puts(',') end + self:tabify() + self:putKey(k) + self:puts(' = ') + self:putValue(t[k]) + count = count + 1 + end + + if mt then + if count > 0 then self:puts(',') end + self:tabify() + self:puts(' = ') + self:putValue(mt) + end + end) + + if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing } + self:tabify() + elseif sequenceLength > 0 then -- array tables have one extra space before closing } + self:puts(' ') + end + + self:puts('}') + end +end + +function Inspector:putValue(v) + local tv = type(v) + + if tv == 'string' then + self:puts(smartQuote(escape(v))) + elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then + self:puts(tostring(v)) + elseif tv == 'table' then + self:putTable(v) + else + self:puts('<',tv,' ',self:getId(v),'>') + end +end + +------------------------------------------------------------------- + +function inspect.inspect(root, options) + options = options or {} + + local depth = options.depth or math.huge + local newline = options.newline or '\n' + local indent = options.indent or ' ' + local process = options.process + + if process then + root = processRecursive(process, root, {}, {}) + end + + local inspector = setmetatable({ + depth = depth, + level = 0, + buffer = {}, + ids = {}, + maxIds = {}, + newline = newline, + indent = indent, + tableAppearances = countTableAppearances(root) + }, Inspector_mt) + + inspector:putValue(root) + + return table.concat(inspector.buffer) +end + +setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) + +return inspect + diff --git a/rockspecs/inspect-1.2-2.rockspec b/rockspecs/inspect-1.2-2.rockspec new file mode 100644 index 0000000..b683143 --- /dev/null +++ b/rockspecs/inspect-1.2-2.rockspec @@ -0,0 +1,23 @@ +package = "inspect" +version = "1.2-2" +source = { + url = "https://github.com/kikito/inspect.lua/archive/v1.2.0.tar.gz", + dir = "inspect.lua-1.2.0" +} +description = { + summary = "Lua table visualizer, ideal for debugging", + detailed = [[ + inspect will print out your lua tables nicely so you can debug your programs quickly. It sorts keys by type and name, handles data recursion + ]], + homepage = "https://github.com/kikito/inspect.lua", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + inspect = "inspect.lua" + } +} diff --git a/rockspecs/inspect-2.0-1.rockspec b/rockspecs/inspect-2.0-1.rockspec new file mode 100644 index 0000000..286bbd5 --- /dev/null +++ b/rockspecs/inspect-2.0-1.rockspec @@ -0,0 +1,23 @@ +package = "inspect" +version = "2.0-1" +source = { + url = "https://github.com/kikito/inspect.lua/archive/v2.0.0.tar.gz", + dir = "inspect.lua-2.0.0" +} +description = { + summary = "Lua table visualizer, ideal for debugging", + detailed = [[ + inspect will print out your lua tables nicely so you can debug your programs quickly. It sorts keys by type and name, handles data recursion + ]], + homepage = "https://github.com/kikito/inspect.lua", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + inspect = "inspect.lua" + } +} diff --git a/rockspecs/inspect-3.0-1.rockspec b/rockspecs/inspect-3.0-1.rockspec new file mode 100644 index 0000000..387d450 --- /dev/null +++ b/rockspecs/inspect-3.0-1.rockspec @@ -0,0 +1,23 @@ +package = "inspect" +version = "3.0-1" +source = { + url = "https://github.com/kikito/inspect.lua/archive/v3.0.0.tar.gz", + dir = "inspect.lua-3.0.0" +} +description = { + summary = "Lua table visualizer, ideal for debugging", + detailed = [[ + inspect will print out your lua tables nicely so you can debug your programs quickly. It sorts keys by type and name and handles recursive tables properly. + ]], + homepage = "https://github.com/kikito/inspect.lua", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + inspect = "inspect.lua" + } +} diff --git a/rockspecs/inspect-3.0-2.rockspec b/rockspecs/inspect-3.0-2.rockspec new file mode 100644 index 0000000..b22ec9c --- /dev/null +++ b/rockspecs/inspect-3.0-2.rockspec @@ -0,0 +1,23 @@ +package = "inspect" +version = "3.0-2" +source = { + url = "https://github.com/kikito/inspect.lua/archive/v3.0.1.tar.gz", + dir = "inspect.lua-3.0.1" +} +description = { + summary = "Lua table visualizer, ideal for debugging", + detailed = [[ + inspect will print out your lua tables nicely so you can debug your programs quickly. It sorts keys by type and name and handles recursive tables properly. + ]], + homepage = "https://github.com/kikito/inspect.lua", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + inspect = "inspect.lua" + } +} diff --git a/rockspecs/inspect-3.0-3.rockspec b/rockspecs/inspect-3.0-3.rockspec new file mode 100644 index 0000000..9e07b01 --- /dev/null +++ b/rockspecs/inspect-3.0-3.rockspec @@ -0,0 +1,23 @@ +package = "inspect" +version = "3.0-3" +source = { + url = "https://github.com/kikito/inspect.lua/archive/v3.0.2.tar.gz", + dir = "inspect.lua-3.0.2" +} +description = { + summary = "Lua table visualizer, ideal for debugging", + detailed = [[ + inspect will print out your lua tables nicely so you can debug your programs quickly. It sorts keys by type and name and handles recursive tables properly. + ]], + homepage = "https://github.com/kikito/inspect.lua", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + inspect = "inspect.lua" + } +} diff --git a/rockspecs/inspect-3.0-4.rockspec b/rockspecs/inspect-3.0-4.rockspec new file mode 100644 index 0000000..56f3d48 --- /dev/null +++ b/rockspecs/inspect-3.0-4.rockspec @@ -0,0 +1,23 @@ +package = "inspect" +version = "3.0-4" +source = { + url = "https://github.com/kikito/inspect.lua/archive/v3.0.3.tar.gz", + dir = "inspect.lua-3.0.3" +} +description = { + summary = "Lua table visualizer, ideal for debugging", + detailed = [[ + inspect will print out your lua tables nicely so you can debug your programs quickly. It sorts keys by type and name and handles recursive tables properly. + ]], + homepage = "https://github.com/kikito/inspect.lua", + license = "MIT " +} +dependencies = { + "lua >= 5.1" +} +build = { + type = "builtin", + modules = { + inspect = "inspect.lua" + } +} diff --git a/spec/inspect_spec.lua b/spec/inspect_spec.lua new file mode 100644 index 0000000..1e4a9e0 --- /dev/null +++ b/spec/inspect_spec.lua @@ -0,0 +1,439 @@ +local inspect = require 'inspect' +local unindent = require 'spec.unindent' +local is_luajit, ffi = pcall(require, 'ffi') +local has_rawlen = type(_G.rawlen) == 'function' + +describe( 'inspect', function() + + describe('numbers', function() + it('works', function() + assert.equals("1", inspect(1)) + assert.equals("1.5", inspect(1.5)) + assert.equals("-3.14", inspect(-3.14)) + end) + end) + + describe('strings', function() + it('puts quotes around regular strings', function() + assert.equals('"hello"', inspect("hello")) + end) + + it('puts apostrophes around strings with quotes', function() + assert.equals("'I have \"quotes\"'", inspect('I have "quotes"')) + end) + + it('uses regular quotes if the string has both quotes and apostrophes', function() + assert.equals('"I have \\"quotes\\" and \'apostrophes\'"', inspect("I have \"quotes\" and 'apostrophes'")) + end) + + it('escapes newlines properly', function() + assert.equals('"I have \\n new \\n lines"', inspect('I have \n new \n lines')) + end) + + it('escapes tabs properly', function() + assert.equals('"I have \\t a tab character"', inspect('I have \t a tab character')) + end) + + it('escapes backspaces properly', function() + assert.equals('"I have \\b a back space"', inspect('I have \b a back space')) + end) + + it('escapes unnamed control characters with 1 or 2 digits', function() + assert.equals('"Here are some control characters: \\0 \\1 \\6 \\17 \\27 \\31"', + inspect('Here are some control characters: \0 \1 \6 \17 \27 \31')) + end) + + it('escapes unnamed control characters with 3 digits when they are followed by numbers', function() + assert.equals('"Control chars followed by digits \\0001 \\0011 \\0061 \\0171 \\0271 \\0311"', + inspect('Control chars followed by digits \0001 \0011 \0061 \0171 \0271 \0311')) + end) + + it('backslashes its backslashes', function() + assert.equals('"I have \\\\ a backslash"', inspect('I have \\ a backslash')) + assert.equals('"I have \\\\\\\\ two backslashes"', inspect('I have \\\\ two backslashes')) + assert.equals('"I have \\\\\\n a backslash followed by a newline"', inspect('I have \\\n a backslash followed by a newline')) + end) + + end) + + it('works with nil', function() + assert.equals('nil', inspect(nil)) + end) + + it('works with functions', function() + assert.equals('{ , , }', inspect({ print, type, print })) + end) + + it('works with booleans', function() + assert.equals('true', inspect(true)) + assert.equals('false', inspect(false)) + end) + + if is_luajit then + it('works with luajit cdata', function() + assert.equals('{ , , }', inspect({ ffi.new("int", 1), ffi.typeof("int"), ffi.typeof("int")(1) })) + end) + end + + describe('tables', function() + + it('works with simple array-like tables', function() + assert.equals("{ 1, 2, 3 }", inspect({1,2,3})) + end) + + it('works with nested arrays', function() + assert.equals('{ "a", "b", "c", { "d", "e" }, "f" }', inspect({'a','b','c', {'d','e'}, 'f'})) + end) + + if has_rawlen then + it('handles arrays with a __len metatable correctly (ignoring the __len metatable and using rawlen)', function() + local arr = setmetatable({1,2,3}, {__len = function() return nil end}) + assert.equals("{ 1, 2, 3,\n = {\n __len = \n }\n}", inspect(arr)) + end) + end + + it('works with simple dictionary tables', function() + assert.equals("{\n a = 1,\n b = 2\n}", inspect({a = 1, b = 2})) + end) + + it('identifies tables with no number 1 as struct-like', function() + assert.equals(unindent([[{ + [2] = 1, + [25] = 1, + id = 1 + } + ]]), inspect({[2]=1,[25]=1,id=1})) + end) + + it('identifies numeric non-array keys as dictionary keys', function() + assert.equals("{ 1, 2,\n [-1] = true\n}", inspect({1, 2, [-1] = true})) + assert.equals("{ 1, 2,\n [1.5] = true\n}", inspect({1, 2, [1.5] = true})) + end) + + it('sorts keys in dictionary tables', function() + local t = { 1,2,3, + [print] = 1, ["buy more"] = 1, a = 1, + [coroutine.create(function() end)] = 1, + [14] = 1, [{c=2}] = 1, [true]= 1 + } + assert.equals(unindent([[ + { 1, 2, 3, + [14] = 1, + [true] = 1, + a = 1, + ["buy more"] = 1, + [{ + c = 2 + }] = 1, + [] = 1, + [] = 1 + } + ]]), inspect(t)) + end) + + it('works with nested dictionary tables', function() + assert.equals(unindent([[{ + a = 1, + b = { + c = 2 + }, + d = 3 + }]]), inspect( {d=3, b={c=2}, a=1} )) + end) + + it('works with hybrid tables', function() + assert.equals(unindent([[ + { "a", { + b = 1 + }, 2, + ["ahoy you"] = 4, + c = 3 + } + ]]), inspect({ 'a', {b = 1}, 2, c = 3, ['ahoy you'] = 4 })) + + + end) + + it('displays
instead of repeating an already existing table', function() + local a = { 1, 2, 3 } + local b = { 'a', 'b', 'c', a } + a[4] = b + a[5] = a + a[6] = b + assert.equals('<1>{ 1, 2, 3, <2>{ "a", "b", "c",
},
,
}', inspect(a)) + end) + + describe('The depth parameter', function() + local level5 = { 1,2,3, a = { b = { c = { d = { e = 5 } } } } } + local keys = { [level5] = true } + + it('has infinite depth by default', function() + assert.equals(unindent([[ + { 1, 2, 3, + a = { + b = { + c = { + d = { + e = 5 + } + } + } + } + } + ]]), inspect(level5)) + end) + it('is modifiable by the user', function() + assert.equals(unindent([[ + { 1, 2, 3, + a = { + b = {...} + } + } + ]]), inspect(level5, {depth = 2})) + + assert.equals(unindent([[ + { 1, 2, 3, + a = {...} + } + ]]), inspect(level5, {depth = 1})) + + assert.equals(unindent([[ + { 1, 2, 3, + a = { + b = { + c = { + d = {...} + } + } + } + } + ]]), inspect(level5, {depth = 4})) + + assert.equals("{...}", inspect(level5, {depth = 0})) + end) + + it('respects depth on keys', function() + assert.equals(unindent([[ + { + [{ 1, 2, 3, + a = { + b = { + c = {...} + } + } + }] = true + } + ]]), inspect(keys, {depth = 4})) + end) + end) + + describe('the newline option', function() + it('changes the substring used for newlines', function() + local t = {a={b=1}} + + assert.equal("{@ a = {@ b = 1@ }@}", inspect(t, {newline='@'})) + end) + end) + + describe('the indent option', function() + it('changes the substring used for indenting', function() + local t = {a={b=1}} + + assert.equal("{\n>>>a = {\n>>>>>>b = 1\n>>>}\n}", inspect(t, {indent='>>>'})) + end) + end) + + describe('the process option', function() + + it('removes one element', function() + local names = {'Andrew', 'Peter', 'Ann' } + local removeAnn = function(item) if item ~= 'Ann' then return item end end + assert.equals('{ "Andrew", "Peter" }', inspect(names, {process = removeAnn})) + end) + + it('uses the path', function() + local names = {'Andrew', 'Peter', 'Ann' } + local removeThird = function(item, path) if path[1] ~= 3 then return item end end + assert.equals('{ "Andrew", "Peter" }', inspect(names, {process = removeThird})) + end) + + it('replaces items', function() + local names = {'Andrew', 'Peter', 'Ann' } + local filterAnn = function(item) return item == 'Ann' and '' or item end + assert.equals('{ "Andrew", "Peter", "" }', inspect(names, {process = filterAnn})) + end) + + it('nullifies metatables', function() + local mt = {'world'} + local t = setmetatable({'hello'}, mt) + local removeMt = function(item) if item ~= mt then return item end end + assert.equals('{ "hello" }', inspect(t, {process = removeMt})) + end) + + it('nullifies metatables using their paths', function() + local mt = {'world'} + local t = setmetatable({'hello'}, mt) + local removeMt = function(item, path) if path[#path] ~= inspect.METATABLE then return item end end + assert.equals('{ "hello" }', inspect(t, {process = removeMt})) + end) + + it('nullifies the root object', function() + local names = {'Andrew', 'Peter', 'Ann' } + local removeNames = function(item) if item ~= names then return item end end + assert.equals('nil', inspect(names, {process = removeNames})) + end) + + it('changes keys', function() + local dict = {a = 1} + local changeKey = function(item) return item == 'a' and 'x' or item end + assert.equals('{\n x = 1\n}', inspect(dict, {process = changeKey})) + end) + + it('nullifies keys', function() + local dict = {a = 1, b = 2} + local removeA = function(item) return item ~= 'a' and item or nil end + assert.equals('{\n b = 2\n}', inspect(dict, {process = removeA})) + end) + + it('prints inspect.KEY & inspect.METATABLE', function() + local t = {inspect.KEY, inspect.METATABLE} + assert.equals("{ inspect.KEY, inspect.METATABLE }", inspect(t)) + end) + + it('marks key paths with inspect.KEY and metatables with inspect.METATABLE', function() + local t = { [{a=1}] = setmetatable({b=2}, {c=3}) } + + local items = {} + local addItem = function(item, path) + items[#items + 1] = {item = item, path = path} + return item + end + + inspect(t, {process = addItem}) + + assert.same({ + {item = t, path = {}}, + {item = {a=1}, path = {{a=1}, inspect.KEY}}, + {item = 'a', path = {{a=1}, inspect.KEY, 'a', inspect.KEY}}, + {item = 1, path = {{a=1}, inspect.KEY, 'a'}}, + {item = setmetatable({b=2}, {c=3}), path = {{a=1}}}, + {item = 'b', path = {{a=1}, 'b', inspect.KEY}}, + {item = 2, path = {{a=1}, 'b'}}, + {item = {c=3}, path = {{a=1}, inspect.METATABLE}}, + {item = 'c', path = {{a=1}, inspect.METATABLE, 'c', inspect.KEY}}, + {item = 3, path = {{a=1}, inspect.METATABLE, 'c'}} + }, items) + + end) + + it('handles recursive tables correctly', function() + local tbl = { 1,2,3} + tbl.loop = tbl + inspect(tbl, { process=function(x) return x end}) + end) + end) + + describe('metatables', function() + + it('includes the metatable as an extra hash attribute', function() + local foo = { foo = 1, __mode = 'v' } + local bar = setmetatable({a = 1}, foo) + assert.equals(unindent([[ + { + a = 1, + = { + __mode = "v", + foo = 1 + } + } + ]]), inspect(bar)) + end) + + it('includes the __tostring metamethod if it exists', function() + local foo = { foo = 1, __tostring = function() return 'hello\nworld' end } + local bar = setmetatable({a = 1}, foo) + assert.equals(unindent([[ + { -- hello\nworld + a = 1, + = { + __tostring = , + foo = 1 + } + } + ]]), inspect(bar)) + end) + + it('includes an error string if __tostring metamethod throws an error', function() + local foo = { foo = 1, __tostring = function() error('hello', 0) end } + local bar = setmetatable({a = 1}, foo) + assert.equals(unindent([[ + { -- error: hello + a = 1, + = { + __tostring = , + foo = 1 + } + } + ]]), inspect(bar)) + end) + + it('does not allow collecting weak tables while they are being inspected', function() + collectgarbage('stop') + finally(function() collectgarbage('restart') end) + local shimMetatable = { + __mode = 'v', + __index = function() return {} end, + __tostring = function() collectgarbage() return 'shim' end + } + local function shim() return setmetatable({}, shimMetatable) end + local t = shim() + t.key = shim() + assert.equals(unindent([[ + { -- shim + key = { -- shim + = <1>{ + __index = , + __mode = "v", + __tostring = + } + }, + =
+ } + ]]), inspect(t)) + end) + + describe('When a table is its own metatable', function() + it('accepts a table that is its own metatable without stack overflowing', function() + local x = {} + setmetatable(x,x) + assert.equals(unindent([[ + <1>{ + =
+ } + ]]), inspect(x)) + end) + + it('can invoke the __tostring method without stack overflowing', function() + local t = {} + t.__index = t + setmetatable(t,t) + assert.equals(unindent([[ + <1>{ + __index =
, + =
+ } + ]]), inspect(t)) + end) + end) + end) + end) + + it('allows changing the global tostring', function() + local save = _G.tostring + _G.tostring = inspect + local s = tostring({1, 2, 3}) + _G.tostring = save + assert.equals("{ 1, 2, 3 }", s) + end) + +end) diff --git a/spec/unindent.lua b/spec/unindent.lua new file mode 100644 index 0000000..02324a1 --- /dev/null +++ b/spec/unindent.lua @@ -0,0 +1,39 @@ +-- Unindenting transforms a string like this: +-- [[ +-- { +-- foo = 1, +-- bar = 2 +-- } +-- ]] +-- +-- Into the same one without indentation, nor start/end newlines +-- +-- [[{ +-- foo = 1, +-- bar = 2 +-- }]] +-- +-- This makes the strings look and read better in the tests +-- + +local getIndentPreffix = function(str) + local level = math.huge + local minPreffix = "" + local len + for preffix in str:gmatch("\n( +)") do + len = #preffix + if len < level then + level = len + minPreffix = preffix + end + end + return minPreffix +end + +local unindent = function(str) + str = str:gsub(" +$", ""):gsub("^ +", "") -- remove spaces at start and end + local preffix = getIndentPreffix(str) + return (str:gsub("\n" .. preffix, "\n"):gsub("\n$", "")) +end + +return unindent