merge with dev

eLua
kikito 10 years ago
commit 363b9db8a2
  1. 240
      README.md
  2. 285
      inspect.lua
  3. 345
      spec/inspect_spec.lua
  4. 39
      spec/unindent.lua

@ -10,90 +10,222 @@ The objective here is human understanding (i.e. for debugging), not serializatio
Examples of use
===============
`inspect` has the following declaration: `str = inspect(value, <options>)`.
`inspect` has the following declaration: `local str = inspect(value, <options>)`.
`value` can be any Lua value. `inspect` transforms simple types (like strings or numbers) into strings. Tables, on the other
hand, are rendered in a way a human can undersand.
`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:
inspect({1,2,3,4}) == "{ 1, 2, 3, 4 }"
```lua
assert(inspect({1,2,3,4}) == "{ 1, 2, 3, 4 }")
```
"dictionary-like" tables are rendered with one element per line:
"Dictionary-like" tables are rendered with one element per line:
inspect({a=1,b=2}) == [[{
a = 1,
b = 2
}]]
```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:
inspect({1,2,3,b=2,a=1}) == [[{ 1, 2, 3,
a = 1,
b = 2
}]]
```lua
assert(inspect({1,2,3,b=2,a=1}) == [[{ 1, 2, 3,
a = 1,
b = 2
}]])
```
Tables can be nested, and will be indented with two spaces per level.
Subtables are indented with two spaces per level.
inspect({a={b=2}}) == [[{
a = {
b = 2
}
}]]
```lua
assert(inspect({a={b=2}}) == [[{
a = {
b = 2
}
}]])
```
Functions, userdata and any other custom types from Luajit are simply as `<function x>`, `<userdata x>`, etc.:
inspect({ f = print, ud = some_user_data, thread = a_thread} ) == [[{
f = <function 1>,
u = <userdata 1>,
thread = <thread 1>
}]])
```lua
assert(inspect({ f = print, ud = some_user_data, thread = a_thread} ) == [[{
f = <function 1>,
u = <userdata 1>,
thread = <thread 1>
}]])
```
If the table has a metatable, inspect will include it at the end, in a special field called `<metatable>`:
inspect(setmetatable({a=1}, {b=2}) == [[{
a = 1
<metatable> = {
b = 2
}
}]])
```lua
assert(inspect(setmetatable({a=1}, {b=2}) == [[{
a = 1
<metatable> = {
b = 2
}
}]]))
```
`inspect` can handle tables with loops inside them. It will print `<id>` right before the table is printed out the first time, and replace the whole table with `<table id>` from then on, preventing infinite loops.
a = {1, 2}
b = {3, 4, a}
a[3] = b -- a references b, and b references a
inspect(a) = "<1>{ 1, 2, { 3, 4, <table 1> } }"
```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, <table 1> } }")
```
Notice that since both `a` appears more than once in the expression, it is prefixed by `<1>` and replaced by `<table 1>` every time it appears later on.
### options.depth
### options
`inspect`'s second parameter allows controlling the maximum depth that will be printed out. When the max depth is reached, it'll just return `{...}`:
`inspect` has a second parameter, called `options`. It is not mandatory, but when it is provided, it must be a table.
local t5 = {a = {b = {c = {d = {e = 5}}}}}
#### options.depth
inspect(t5, {depth = 4}) == [[{
a = {
b = {
c = {
d = {...}
}
}
}
}]]
`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
inspect(t5, {depth = 2}) == [[{
a = {
b = {...}
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.filter
### 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 `path`:
``` lua
local t, mt = ... -- (defined as before)
local remove_all_metatables = function(item, path)
if path[#path] ~= '<metatable>' then return item end
end
-- Removes all metatables
assert(inspect(t, {process = remove_mt}) == "{ 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"
}]])
```
Sometimes it might be convenient to "filter out" some parts of the output. The `options.filter` option can do that.
@ -116,14 +248,14 @@ Sometimes it might be convenient to "filter out" some parts of the output. The `
Gotchas / Warnings
==================
This method is *not* appropiate for saving/restoring tables. It is ment to be used by the programmer mainly while debugging a program.
This method is *not* appropriate for saving/restoring tables. It is meant to be used by the programmer mainly while debugging a program.
Installation
============
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, altough others might like table.inspect)
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 --
@ -134,7 +266,7 @@ Also, make sure to read the license file; the text of that license file must app
Specs
=====
This project uses [busted](http://olivinelabs.com/busted/) for its specs. If you want to run the specs, you will have to install telescope first. Then just execute the following from the root inspect folder:
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

@ -28,6 +28,9 @@ local inspect ={
]]
}
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)
@ -136,159 +139,191 @@ local function countTableAppearances(t, tableAppearances)
return tableAppearances
end
local function parse_filter(filter)
if type(filter) == 'function' then return filter end
-- not a function, so it must be a table or table-like
filter = type(filter) == 'table' and filter or {filter}
local dictionary = {}
for _,v in pairs(filter) do dictionary[v] = true end
return function(x) return dictionary[x] 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, key)
local newPath, len = {}, #path
for i=1, len do newPath[i] = path[i] end
newPath[len+1] = key
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
-------------------------------------------------------------------
function inspect.inspect(rootObject, options)
options = options or {}
local depth = options.depth or math.huge
local filter = parse_filter(options.filter or {})
local tableAppearances = countTableAppearances(rootObject)
local function processRecursive(process, item, path)
if item == nil then return nil end
local buffer = {}
local maxIds = setmetatable({}, maxIdsMetaTable)
local ids = setmetatable({}, idsMetaTable)
local level = 0
local blen = 0 -- buffer length
local processed = process(item, path)
if type(processed) == 'table' then
local processedCopy = {}
local processedKey
local function puts(...)
local args = {...}
for i=1, #args do
blen = blen + 1
buffer[blen] = tostring(args[i])
for k,v in pairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY))
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey))
end
end
end
local function down(f)
level = level + 1
f()
level = level - 1
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE))
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
local function tabify()
puts("\n", string.rep(" ", level))
end
local function commaControl(needsComma)
if needsComma then puts(',') end
return true
end
-------------------------------------------------------------------
local function alreadyVisited(v)
return ids[type(v)][v] ~= nil
end
local Inspector = {}
local Inspector_mt = {__index = Inspector}
local function getId(v)
local tv = type(v)
local id = ids[tv][v]
if not id then
id = maxIds[tv] + 1
maxIds[tv] = id
ids[tv][v] = id
end
return id
function Inspector:puts(...)
local args = {...}
local buffer = self.buffer
local len = #buffer
for i=1, #args do
len = len + 1
buffer[len] = tostring(args[i])
end
end
function Inspector:down(f)
self.level = self.level + 1
f()
self.level = self.level - 1
end
local putValue -- forward declaration that needs to go before putTable & putKey
function Inspector:tabify()
self:puts(self.newline, string.rep(self.indent, self.level))
end
function Inspector:commaControl(needsComma)
if needsComma then self:puts(',') end
return true
end
function Inspector:alreadyVisited(v)
return self.ids[type(v)][v] ~= nil
end
local function putKey(k)
if isIdentifier(k) then return puts(k) end
puts( "[" )
putValue(k, {})
puts("]")
function Inspector:getId(v)
local tv = type(v)
local id = self.ids[tv][v]
if not id then
id = self.maxIds[tv] + 1
self.maxIds[tv] = id
self.ids[tv][v] = id
end
return id
end
local function putTable(t, path)
if alreadyVisited(t) then
puts('<', type(t), ' ', getId(t), '>')
elseif level >= depth then
puts('{...}')
else
if tableAppearances[t] > 1 then puts('<', getId(t), '>') end
local dictKeys = getDictionaryKeys(t)
local length = #t
local mt = getmetatable(t)
local to_string_result = getToStringResultSafely(t, mt)
puts('{')
down(function()
if to_string_result then
puts(' -- ', escape(to_string_result))
if length >= 1 then tabify() end -- tabify the array values
end
local needsComma = false
for i=1, length do
needsComma = commaControl(needsComma)
puts(' ')
putValue(t[i], makePath(path, i))
end
for _,k in ipairs(dictKeys) do
needsComma = commaControl(needsComma)
tabify()
putKey(k)
puts(' = ')
putValue(t[k], makePath(path, k))
end
if mt then
needsComma = commaControl(needsComma)
tabify()
puts('<metatable> = ')
putValue(mt, makePath(path, '<metatable>'))
end
end)
if #dictKeys > 0 or mt then -- dictionary table. Justify closing }
tabify()
elseif length > 0 then -- array tables have one extra space before closing }
puts(' ')
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('<table ', self:getId(t), '>')
elseif self.level >= self.depth then
self:puts('{...}')
else
if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
local dictKeys = getDictionaryKeys(t)
local length = #t
local mt = getmetatable(t)
local to_string_result = getToStringResultSafely(t, mt)
self:puts('{')
self:down(function()
if to_string_result then
self:puts(' -- ', escape(to_string_result))
if length >= 1 then self:tabify() end
end
puts('}')
end
local needsComma = false
for i=1, length do
needsComma = self:commaControl(needsComma)
self:puts(' ')
self:putValue(t[i])
end
end
for _,k in ipairs(dictKeys) do
needsComma = self:commaControl(needsComma)
self:tabify()
self:putKey(k)
self:puts(' = ')
self:putValue(t[k])
end
-- putvalue is forward-declared before putTable & putKey
putValue = function(v, path)
if filter(v, path) then
puts('<filtered>')
else
local tv = type(v)
if tv == 'string' then
puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then
puts(tostring(v))
elseif tv == 'table' or tv == 'romtable' then
putTable(v, path)
else
puts('<',tv,' ',getId(v),'>')
if mt then
needsComma = self:commaControl(needsComma)
self:tabify()
self:puts('<metatable> = ')
self:putValue(mt)
end
end)
if #dictKeys > 0 or mt then -- dictionary table. Justify closing }
self:tabify()
elseif length > 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' or tv == 'romtable' 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 process = options.process
local newline = options.newline or '\n'
local indent = options.indent or ' '
if process then
root = processRecursive(process, root, {})
end
local inspector = setmetatable({
depth = depth,
buffer = {},
level = 0,
ids = setmetatable({}, idsMetaTable),
maxIds = setmetatable({}, maxIdsMetaTable),
newline = newline,
indent = indent,
tableAppearances = countTableAppearances(root)
}, Inspector_mt)
putValue(rootObject, {})
inspector:putValue(root)
return table.concat(buffer)
return table.concat(inspector.buffer)
end
setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })

@ -1,5 +1,6 @@
local inspect = require 'inspect'
local is_luajit, ffi = pcall(require, 'ffi')
local inspect = require 'inspect'
local unindent = require 'spec.unindent'
local is_luajit, ffi = pcall(require, 'ffi')
describe( 'inspect', function()
@ -82,41 +83,45 @@ describe( 'inspect', function()
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
}
local s = [[{ 1, 2, 3,
[14] = 1,
[true] = 1,
a = 1,
["buy more"] = 1,
[{
c = 2
}] = 1,
[<function 1>] = 1]]
if is_luajit then
t[ffi.new("int", 1)] = 1
s = s .. ",\n [<cdata 1>] = 1"
end
assert.equals(inspect(t), s .. "\n}")
assert.equals(inspect(t), unindent([[
{ 1, 2, 3,
[14] = 1,
[true] = 1,
a = 1,
["buy more"] = 1,
[{
c = 2
}] = 1,
[<function 1>] = 1,
[<thread 1>] = 1
}
]]))
end)
it('works with nested dictionary tables', function()
assert.equals(inspect( {d=3, b={c=2}, a=1} ), [[{
a = 1,
b = {
c = 2
},
d = 3
}]])
assert.equals(inspect( {d=3, b={c=2}, a=1} ), unindent([[{
a = 1,
b = {
c = 2
},
d = 3
}]]))
end)
it('works with hybrid tables', function()
assert.equals(inspect({ 'a', {b = 1}, 2, c = 3, ['ahoy you'] = 4 }), [[{ "a", {
b = 1
}, 2,
["ahoy you"] = 4,
c = 3
}]])
assert.equals(
inspect({ 'a', {b = 1}, 2, c = 3, ['ahoy you'] = 4 }),
unindent([[
{ "a", {
b = 1
}, 2,
["ahoy you"] = 4,
c = 3
}
]]))
end)
it('displays <table x> instead of repeating an already existing table', function()
@ -133,124 +138,163 @@ describe( 'inspect', function()
local keys = { [level5] = true }
it('has infinite depth by default', function()
assert.equals(inspect(level5), [[{ 1, 2, 3,
a = {
b = {
c = {
d = {
e = 5
}
}
}
}
}]])
assert.equals(inspect(level5), unindent([[
{ 1, 2, 3,
a = {
b = {
c = {
d = {
e = 5
}
}
}
}
}
]]))
end)
it('is modifiable by the user', function()
assert.equals(inspect(level5, {depth = 2}), [[{ 1, 2, 3,
a = {
b = {...}
}
}]])
assert.equals(inspect(level5, {depth = 1}), [[{ 1, 2, 3,
a = {...}
}]])
assert.equals(inspect(level5, {depth = 0}), "{...}")
assert.equals(inspect(level5, {depth = 4}), [[{ 1, 2, 3,
a = {
b = {
c = {
d = {...}
}
}
}
}]])
assert.equals(inspect(level5, {depth = 2}), unindent([[
{ 1, 2, 3,
a = {
b = {...}
}
}
]]))
assert.equals(inspect(level5, {depth = 1}), unindent([[
{ 1, 2, 3,
a = {...}
}
]]))
assert.equals(inspect(level5, {depth = 4}), unindent([[
{ 1, 2, 3,
a = {
b = {
c = {
d = {...}
}
}
}
}
]]))
assert.equals(inspect(level5, {depth = 0}), "{...}")
end)
it('respects depth on keys', function()
assert.equals(inspect(keys, {depth = 4}), [[{
[{ 1, 2, 3,
a = {
b = {
c = {...}
}
}
}] = true
}]])
assert.equals(inspect(keys, {depth = 4}), unindent([[
{
[{ 1, 2, 3,
a = {
b = {
c = {...}
}
}
}] = true
}
]]))
end)
end)
describe('The filter option', function()
it('filters hash values', function()
local a = {'this is a'}
local b = {x = 1, a = a}
describe('the newline option', function()
it('changes the substring used for newlines', function()
local t = {a={b=1}}
assert.equals(inspect(b, {filter = {a}}), [[{
a = <filtered>,
x = 1
}]])
assert.equal(inspect(t, {newline='@'}), "{@ a = {@ b = 1@ }@}")
end)
end)
it('filtereds hash keys', function()
local a = {'this is a'}
local b = {x = 1, [a] = 'a is used as a key here'}
describe('the indent option', function()
it('changes the substring used for indenting', function()
local t = {a={b=1}}
assert.equals(inspect(b, {filter = {a}}), [[{
x = 1,
[<filtered>] = "a is used as a key here"
}]])
assert.equal(inspect(t, {indent='>>>'}), "{\n>>>a = {\n>>>>>>b = 1\n>>>}\n}")
end)
end)
it('filtereds array values', function()
assert.equals(inspect({10,20,30}, {filter = {20}}), "{ 10, <filtered>, 30 }")
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(inspect(names, {process = removeAnn}), '{ "Andrew", "Peter" }')
end)
it('filtereds metatables', function()
local a = {'this is a'}
local b = setmetatable({x = 1}, a)
assert.equals(inspect(b, {filter = {a}}), [[{
x = 1,
<metatable> = <filtered>
}]])
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(inspect(names, {process = removeThird}), '{ "Andrew", "Peter" }')
end)
it('replaces items', function()
local names = {'Andrew', 'Peter', 'Ann' }
local filterAnn = function(item) return item == 'Ann' and '<filtered>' or item end
assert.equals(inspect(names, {process = filterAnn}), '{ "Andrew", "Peter", "<filtered>" }')
end)
it('filters by path', function()
local people = { tony = { age = 21 }, martha = { age = 34} }
local hideMarthaAge = function(_,path)
return table.concat(path, '.') == 'martha.age'
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(inspect(t, {process = removeMt}), '{ "hello" }')
end)
assert.equals(inspect(people, {filter = hideMarthaAge}), [[{
martha = {
age = <filtered>
},
tony = {
age = 21
}
}]])
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(inspect(t, {process = removeMt}), '{ "hello" }')
end)
it('does not increase the table ids', function()
local a = {'this is a'}
local b = {}
local c = {a, b, b}
assert.equals(inspect(c, {filter = {a}}), "{ <filtered>, <1>{}, <table 1> }")
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(inspect(names, {process = removeNames}), 'nil')
end)
it('can be a non table (gets interpreted as a table with one element)', function()
assert.equals(inspect({'foo', 'bar', 'baz'}, {filter = "bar"}), '{ "foo", <filtered>, "baz" }')
it('changes keys', function()
local dict = {a = 1}
local changeKey = function(item, path) return item == 'a' and 'x' or item end
assert.equals(inspect(dict, {process = changeKey}), '{\n x = 1\n}')
end)
it('can be a function which returns true for the elements that needs to be filtered', function()
local msg = inspect({1,2,3,4,5}, { filter = function(x)
return type(x) == 'number' and x % 2 == 0
end })
it('nullifies keys', function()
local dict = {a = 1, b = 2}
local removeA = function(item, path) return item ~= 'a' and item or nil end
assert.equals(inspect(dict, {process = removeA}), '{\n b = 2\n}')
end)
assert.equals(msg, '{ 1, <filtered>, 3, <filtered>, 5 }')
it('prints inspect.KEY & inspect.METATABLE', function()
local t = {inspect.KEY, inspect.METATABLE}
assert.equals(inspect(t), "{ inspect.KEY, inspect.METATABLE }")
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(items, {
{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'}}
})
end)
end)
describe('metatables', function()
@ -258,58 +302,67 @@ describe( 'inspect', 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(inspect(bar), [[{
a = 1,
<metatable> = {
__mode = "v",
foo = 1
}
}]])
assert.equals(inspect(bar), unindent([[
{
a = 1,
<metatable> = {
__mode = "v",
foo = 1
}
}
]]))
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(inspect(bar), [[{ -- hello\nworld
a = 1,
<metatable> = {
__tostring = <function 1>,
foo = 1
}
}]])
assert.equals(inspect(bar), unindent([[
{ -- hello\nworld
a = 1,
<metatable> = {
__tostring = <function 1>,
foo = 1
}
}
]]))
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(inspect(bar), [[{ -- error: hello
a = 1,
<metatable> = {
__tostring = <function 1>,
foo = 1
}
}]])
assert.equals(inspect(bar), unindent([[
{ -- error: hello
a = 1,
<metatable> = {
__tostring = <function 1>,
foo = 1
}
}
]]))
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(inspect(x), [[<1>{
<metatable> = <table 1>
}]])
assert.equals(inspect(x), unindent([[
<1>{
<metatable> = <table 1>
}
]]))
end)
it('can invoke the __tostring method without stack overflowing', function()
local t = {}
t.__index = t
setmetatable(t,t)
assert.equals(inspect(t), [[<1>{
__index = <table 1>,
<metatable> = <table 1>
}]])
assert.equals(inspect(t), unindent([[
<1>{
__index = <table 1>,
<metatable> = <table 1>
}
]]))
end)
end)
end)
end)

@ -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
Loading…
Cancel
Save