Feat: Add Basic Lua Test Suite (#115)

This MR adds a Lua test suite to the project, run via busted, and introduces tests for a number of the utility functions. Subsequent work will have to be done to test functions that use the `vim.api` scope and external packages to the plugin.
This commit is contained in:
Harrison (Harry) Cramer
2023-11-20 18:03:35 -05:00
committed by GitHub
parent 1abc33d149
commit 88b9196a2e
12 changed files with 401 additions and 180 deletions

13
.busted Normal file
View File

@@ -0,0 +1,13 @@
return {
_all = {
pattern = "_spec",
lpath = "lua/?.lua;lua/?/init.lua",
ROOT = {"lua/gitlab"},
},
default = {
verbose = true
},
tests = {
verbose = true,
},
}

View File

@@ -4,7 +4,23 @@ on:
branches:
- main
jobs:
build:
go_lint:
name: Lint Go 💅
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.19'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54
only-new-issues: true
go_test:
name: Test Go 🧪
needs: [go_lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

15
.github/workflows/lua-tests.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Lua Tests
on:
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run Busted
uses: lunarmodules/busted@v2.2.0
# with:
# args: --run=api

View File

@@ -1,10 +1,11 @@
name: Linting and Formatting
name: Lua
on:
pull_request:
branches:
- main
jobs:
luacheck:
lua_lint:
name: Lint Lua 💅
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -13,7 +14,8 @@ jobs:
uses: lunarmodules/luacheck@v1
with:
args: --globals vim --no-max-line-length -- .
stylua:
lua_format:
name: Formatting Lua 💅
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -24,16 +26,12 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
args: --check .
golangci:
lua_test:
name: Test Lua 🧪
needs: [lua_format,lua_lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.19'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54
only-new-issues: true
- name: Checkout
uses: actions/checkout@v3
- name: Run Busted
uses: lunarmodules/busted@v2.2.0

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
bin
.luarc.json
/luarocks
/lua_modules
/.luarocks

1
logs
View File

@@ -1 +0,0 @@
[DEBUG] GET https://gitlab.com/api/v4/merge_requests?source_branch=develop&state=opened

View File

@@ -892,7 +892,7 @@ end
---@param note Note
---@return string
M.build_note_header = function(note)
return "@" .. note.author.username .. " " .. u.format_date(note.created_at)
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
end
M.build_note_body = function(note, resolve_info)
@@ -977,7 +977,7 @@ M.add_discussions_to_table = function(items)
end
-- Creates the first node in the discussion, and attaches children
local body = u.join_tables(root_text_nodes, discussion_children)
local body = u.spread(root_text_nodes, discussion_children)
local root_node = NuiTree.Node({
text = root_text,
is_note = true,

View File

@@ -46,7 +46,7 @@ M.open = function()
u.switch_can_edit_buf(bufnr, true)
table.insert(lines, string.format("Status: %s (%s)", state.settings.pipeline[pipeline.status], pipeline.status))
table.insert(lines, "")
table.insert(lines, string.format("Last Run: %s", u.format_date(pipeline.created_at)))
table.insert(lines, string.format("Last Run: %s", u.time_since(pipeline.created_at)))
table.insert(lines, string.format("Url: %s", pipeline.web_url))
table.insert(lines, string.format("Triggered By: %s", pipeline.source))
@@ -99,7 +99,7 @@ M.see_logs = function()
local bufnr = vim.api.nvim_get_current_buf()
local linnr = vim.api.nvim_win_get_cursor(0)[1]
local text = u.get_line_content(bufnr, linnr)
local last_word = u.get_last_chunk(text)
local last_word = u.get_last_word(text)
if last_word == nil then
u.notify("Cannot find job name", vim.log.levels.ERROR)
return

View File

@@ -0,0 +1,181 @@
describe("utils/init.lua", function()
it("Loads package", function()
local utils_ok, _ = pcall(require, "gitlab.utils")
assert._is_true(utils_ok)
end)
local _, u = pcall(require, "gitlab.utils")
describe("extract", function()
it("Extracts a single value", function()
local t = { { one = 1, two = 2 }, { three = 3, four = 4 } }
local got = u.extract(t, "one")
local want = { 1 }
assert.are.same(want, got)
end)
it("Returns nothing with empty table", function()
local t = {}
local got = u.extract(t, "one")
local want = {}
assert.are.same(want, got)
end)
end)
describe("get_last_word", function()
it("Returns the last word in a sentence", function()
local sentence = "Hello world!"
local got = u.get_last_word(sentence)
local want = "world!"
assert.are.same(want, got)
end)
it("Returns an empty string without text", function()
local sentence = ""
local got = u.get_last_word(sentence)
local want = ""
assert.are.same(want, got)
end)
it("Returns whole string w/out divider", function()
local sentence = "Thisdoesnothavebreaks"
local got = u.get_last_word(sentence)
assert.are.same(sentence, got)
end)
it("Returns correct word w/ different divider", function()
local sentence = "this|uses|a|different|divider"
local got = u.get_last_word(sentence, "|")
local want = "divider"
assert.are.same(want, got)
end)
end)
describe("format_date", function()
local current_date = {
day = 19,
hour = 22,
isdst = false,
min = 0,
month = 11,
sec = 44,
wday = 1,
yday = 323,
year = 2023,
}
it("Returns days since a valid UTC timestamp", function()
local stamp = "2023-11-16T19:52:36.946Z"
local got = u.time_since(stamp, current_date)
local want = "3 days ago"
assert.are.same(want, got)
end)
it("Returns hours since a valid UTC timestamp", function()
local stamp = "2023-11-19T19:52:36.946Z"
local got = u.time_since(stamp, current_date)
local want = "2 hours ago"
assert.are.same(want, got)
end)
it("Returns readable time if > 1 year", function()
local stamp = "2011-11-19T19:52:36.946Z"
local got = u.time_since(stamp, current_date)
local want = "November 19, 2011"
assert.are.same(want, got)
end)
end)
describe("remove_first_value", function()
it("Removes the first value correctly", function()
local got = u.remove_first_value({ 1, 2 })
local want = { 2 }
assert.are.same(want, got)
end)
it("Handles a one-length list", function()
local got = u.remove_first_value({ 1 })
local want = {}
assert.are.same(want, got)
end)
it("Handles a zero-length list", function()
local got = u.remove_first_value({})
local want = {}
assert.are.same(want, got)
end)
end)
describe("table_size", function()
it("Works for associative tables", function()
local got = u.remove_first_value({ 1, 2 })
local want = { 2 }
assert.are.same(want, got)
end)
it("Handles a one-length list", function()
local got = u.remove_first_value({ 1 })
local want = {}
assert.are.same(want, got)
end)
it("Handles a zero-length list", function()
local got = u.remove_first_value({})
local want = {}
assert.are.same(want, got)
end)
end)
describe("contains", function()
it("Finds a value in a list", function()
local got = u.contains({ 1, 2 }, 1)
assert._is_true(got)
end)
it("Handles missing values", function()
local got = u.contains({ 1, 3, 4 }, 2)
assert._is_false(got)
end)
it("Handles empty lists", function()
local got = u.contains({}, 1)
assert._is_false(got)
end)
end)
describe("reverse", function()
it("Reverses the values in a list", function()
local got = u.reverse({ 1, 2, 3, 4 })
local want = { 4, 3, 2, 1 }
assert.are.same(got, want)
end)
it("Handles single value", function()
local got = u.reverse({ 1 })
local want = { 1 }
assert.are.same(got, want)
end)
it("Handles empty list", function()
local got = u.reverse({})
local want = {}
assert.are.same(got, want)
end)
end)
describe("spread", function()
it("Spreads the values", function()
local t1 = { 1, 2, 3 }
local t2 = { 4, 5, 6 }
local got = u.spread(t1, t2)
local want = { 1, 2, 3, 4, 5, 6 }
assert.are.same(got, want)
end)
it("Handles an empty t1 table", function()
local t1 = {}
local t2 = { 4, 5, 6 }
local got = u.spread(t1, t2)
local want = { 4, 5, 6 }
assert.are.same(got, want)
end)
it("Handles an empty t2 table", function()
local t1 = { 1, 2, 3 }
local t2 = {}
local got = u.spread(t1, t2)
local want = { 1, 2, 3 }
assert.are.same(got, want)
end)
it("Handles both empty tables", function()
local t1 = {}
local t2 = {}
local got = u.spread(t1, t2)
local want = {}
assert.are.same(got, want)
end)
end)
end)

View File

@@ -122,7 +122,7 @@ M.merge_settings = function(args)
end
M.print_settings = function()
u.P(M.settings)
vim.print(M.settings)
end
-- First reads environment variables into the settings module,

View File

@@ -1,6 +1,156 @@
local Job = require("plenary.job")
local M = {}
---Pulls out a list of values matching a given key from an array of tables
---@param t table List of tables to search
---@param key string Value to search for in the list
---@return table List List of values that were extracted
M.extract = function(t, key)
local resultTable = {}
for _, value in ipairs(t) do
if value[key] then
table.insert(resultTable, value[key])
end
end
return resultTable
end
---Get the last word in a sentence
---@param sentence string The string to get the last word from
---@param divider string The regex to split the sentence by, defaults to whitespace
---@return string
M.get_last_word = function(sentence, divider)
local words = {}
local pattern = string.format("([^%s]+)", divider or " ")
for word in sentence:gmatch(pattern) do
table.insert(words, word)
end
return words[#words] or ""
end
---Merges two deeply nested tables together, overriding values from the first with conflicts
---@param defaults table The first table
---@param overrides table The second table
---@return table
M.merge = function(defaults, overrides)
if type(defaults) == "table" and M.table_size(defaults) == 0 and type(overrides) == "table" then
return overrides
end
return vim.tbl_deep_extend("force", defaults, overrides)
end
---Pluralizes the input word, e.g. "3 cows"
---@param num integer The count of the item/word
---@param word string The word to pluralize
---@return string
local function pluralize(num, word)
return num .. string.format(" %s", word) .. ((num > 1 or num <= 0) and "s" or "")
end
--- Provides a human readable time since a given ISO date string
---@param date_string string -- The ISO time stamp to compare with the current time
---@return string
M.time_since = function(date_string, current_date_table)
local dt = current_date_table or os.date("!*t")
local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
local date = os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
local current_date = os.time({
year = dt.year,
month = dt.month,
day = dt.day,
hour = dt.hour,
min = dt.min,
sec = dt.sec,
})
local time_diff = current_date - date
if time_diff < 60 then
return pluralize(time_diff, "second") .. " ago"
elseif time_diff < 3600 then
return pluralize(math.floor(time_diff / 60), "minute") .. " ago"
elseif time_diff < 86400 then
return pluralize(math.floor(time_diff / 3600), "hour") .. " ago"
elseif time_diff < 2592000 then
return pluralize(math.floor(time_diff / 86400), "day") .. " ago"
else
local formatted_date = os.date("%B %e, %Y", date)
return tostring(formatted_date)
end
end
---Removes the first value from a list and returns the new, smaller list
---@param tbl table The table
---@return table
M.remove_first_value = function(tbl)
local sliced_list = {}
if M.table_size(tbl) <= 1 then
return sliced_list
end
for i = 2, #tbl do
table.insert(sliced_list, tbl[i])
end
return sliced_list
end
---Spreads all the values from t2 into t1
---@param t1 table The first table (gets the values)
---@param t2 table The second table
---@return table
M.spread = function(t1, t2)
for _, value in ipairs(t2) do
table.insert(t1, value)
end
return t1
end
---Returns the number of keys or values in a table
---@param t table The table to count
---@return integer
M.table_size = function(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
---Returns whether a given value is in a list or not
---@param list table The list to search
---@return boolean
M.contains = function(list, search_value)
for _, value in pairs(list) do
if value == search_value then
return true
end
end
return false
end
---Trims whitespace from a string
---@param s string The string to trim
---@return string
M.trim = function(s)
local res = s:gsub("^%s+", ""):gsub("%s+$", "")
return res
end
-- Reverses the order of elements in a list
---@param list table The list to reverse
---@return table
M.reverse = function(list)
if #list == 0 then
return list
end
local rev = {}
for i = #list, 1, -1 do
rev[#rev + 1] = list[i]
end
return rev
end
M.notify = function(msg, lvl)
vim.notify("gitlab.nvim: " .. msg, lvl)
end
@@ -22,65 +172,12 @@ M.is_windows = function()
return false
end
M.P = function(...)
local objects = {}
for i = 1, select("#", ...) do
local v = select(i, ...)
table.insert(objects, vim.inspect(v))
end
print(table.concat(objects, "\n"))
return ...
end
M.get_buffer_text = function(bufnr)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local text = table.concat(lines, "\n")
return text
end
M.string_starts = function(str, start)
return str:sub(1, #start) == start
end
M.press_enter = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<CR>", false, true, true), "n", false)
end
M.format_date = function(date_string)
local date_table = os.date("!*t")
local year, month, day, hour, min, sec = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)")
local date = os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
local current_date = os.time({
year = date_table.year,
month = date_table.month,
day = date_table.day,
hour = date_table.hour,
min = date_table.min,
sec = date_table.sec,
})
local time_diff = current_date - date
local function pluralize(num, word)
return num .. string.format(" %s", word) .. (num > 1 and "s" or "") .. " ago"
end
if time_diff < 60 then
return pluralize(time_diff, "second")
elseif time_diff < 3600 then
return pluralize(math.floor(time_diff / 60), "minute")
elseif time_diff < 86400 then
return pluralize(math.floor(time_diff / 3600), "hour")
elseif time_diff < 2592000 then
return pluralize(math.floor(time_diff / 86400), "day")
else
local formatted_date = os.date("%A, %B %e", date)
return formatted_date
end
end
M.jump_to_file = function(filename, line_number)
if line_number == nil then
line_number = 1
@@ -123,38 +220,6 @@ M.create_popup_state = function(title, width, height)
}
end
M.merge = function(defaults, overrides)
if type(defaults) == "table" and M.table_size(defaults) == 0 and type(overrides) == "table" then
return overrides
end
return vim.tbl_deep_extend("force", defaults, overrides)
end
M.join = function(tbl, separator)
separator = separator or " "
local result = ""
for _, value in pairs(tbl) do
result = result .. tostring(value) .. separator
end
-- Remove the trailing separator
if separator ~= "" then
result = result:sub(1, -#separator - 1)
end
return result
end
M.remove_first_value = function(tbl)
local sliced_table = {}
for i = 2, #tbl do
table.insert(sliced_table, tbl[i])
end
return sliced_table
end
M.read_file = function(file_path)
local file = io.open(file_path, "r")
if file == nil then
@@ -180,41 +245,6 @@ M.uuid = function()
end)
end
M.join_tables = function(table1, table2)
for _, value in ipairs(table2) do
table.insert(table1, value)
end
return table1
end
M.table_size = function(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
M.contains = function(array, search_value)
for _, value in ipairs(array) do
if value == search_value then
return true
end
end
return false
end
M.extract = function(t, property)
local resultTable = {}
for _, value in ipairs(t) do
if value[property] then
table.insert(resultTable, value[property])
end
end
return resultTable
end
M.remove_last_chunk = function(sentence)
local words = {}
for word in sentence:gmatch("%S+") do
@@ -225,26 +255,6 @@ M.remove_last_chunk = function(sentence)
return sentence_without_last
end
M.get_first_chunk = function(sentence, divider)
local words = {}
for word in sentence:gmatch(divider or "%S+") do
table.insert(words, word)
end
return words[1]
end
M.get_last_chunk = function(sentence, divider)
local words = {}
for word in sentence:gmatch(divider or "%S+") do
table.insert(words, word)
end
return words[#words]
end
M.trim = function(s)
return s:gsub("^%s+", ""):gsub("%s+$", "")
end
M.get_line_content = function(bufnr, start)
local current_buffer = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(bufnr ~= nil and bufnr or current_buffer, start - 1, start, false)
@@ -297,14 +307,6 @@ M.list_files_in_folder = function(folder_path)
return result
end
M.reverse = function(list)
local rev = {}
for i = #list, 1, -1 do
rev[#rev + 1] = list[i]
end
return rev
end
---@class Hunk
---@field old_line integer
---@field old_range integer
@@ -318,6 +320,8 @@ end
M.parse_hunk_headers = function(file_path, base_branch)
local hunks = {}
local Job = require("plenary.job")
local diff_job = Job:new({
command = "git",
args = { "diff", "--minimal", "--unified=0", "--no-color", base_branch, "--", file_path },
@@ -430,7 +434,7 @@ M.get_lines_from_hunks = function(hunks, target_line, is_new)
end
---Check if current mode is visual mode
---@return boolean true if current mode is visual mode
---@return boolean is_visual true if current mode is visual mode
M.check_visual_mode = function()
local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" and mode ~= "V" then
@@ -442,7 +446,7 @@ end
---Return start line and end line of visual selection.
---Exists visual mode in order to access marks "<" , ">"
---@return integer,integer start line and end line
---@return integer start,integer end Start line and end line
M.get_visual_selection_boundaries = function()
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Esc>", false, true, true), "nx", false)
local start_line = vim.api.nvim_buf_get_mark(0, "<")[1]

View File

@@ -1,8 +0,0 @@
## Todo
- Screenshot folder in config (where the images will be kept)
- Within the Summary view, you can call the add_summary_image() command
- This command will open a UI picker to choose the file
- When you choose the file, we pass that file path to an API endpoint which uploads
the file and returns the JSON in the API here (https://docs.gitlab.com/ee/api/projects.html#upload-a-file)
- Then we write that into the Summary buffer at the current cursor