mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
infrastructure: Add tests for the core and misc Lua functions that had none (#9802)
#### Brief overview of PR changes/additions - 139 new busted specs (Miscallaneous_spec +126, Package_spec +13) for the last core and misc Lua functions with no test presence at all: the profile description/icon/stats/list accessors, time, process and encoding info, logging, file watches, the user dictionary and spell checking, `unzipAsync`, `loadReplay`, `findItems`, `insertHTML`, `send`, `denyCurrentSend`, `isAncestorsActive`, `raiseGlobalEvent` and the verbose-install / drag-and-drop package helpers. - Contract plus effect wherever the effect can be seen offline - the file that was written, the event that was raised, the line that reached the console - with no mocking. The functions that reach a browser, the tray, a modal dialog or the keyboard get their refusals covered instead, which is all that can be driven headlessly. - Six bugs turned up while writing them. None is specced: five are marked `pending` with the reason so they light up when fixed. #### Motivation for adding to Mudlet Wave 4 of the Lua API test coverage program. Nothing in the suite touched these functions, so a regression in any of them was invisible. #### Other info (issues closed, discussion etc) Bugs found, all left unspecced: - `setSaveCommandHistory()` and `setSaveCommandHistory(name)` raise instead of turning saving on; both count their arguments one too high, and the branch that would read the boolean after a name is unreachable. - `setProfileInformation` and `clearProfileInformation` for a profile that does not exist return true and create the folder, so a phantom profile appears in the connection dialog and in `getProfiles()`. - `raiseGlobalEvent` refuses an unsupported argument with `lua_error()` after building the event, which longjmps past the event's destructor and leaks it. - `insertHTML` hands its text straight to `insertText`, so the markup its name and the wiki promise is printed literally. - `verbosePackageInstall` strips the profile folder off the announced name using that folder as a Lua pattern, so a profile path holding a `-` gets the whole path announced. **Test case:** full busted suite 2544 passed / 0 failed / 136 pending - green on a fresh profile, again on that same profile, and once more on another fresh one (+2.5s of runtime); 97 of the 139 new specs were each shown to fail against a matching sabotage of the C++ or Lua behaviour, which was then reverted. Assisted-by: Claude:claude-opus-5
This commit is contained in:
parent
6c0e399a9a
commit
e42bd1e285
2 changed files with 1691 additions and 0 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -118,6 +118,29 @@ local function fileExists(path)
|
|||
return lfs.attributes(path, "mode") ~= nil
|
||||
end
|
||||
|
||||
-- Everything the main console gained since it was at line `mark`, joined up.
|
||||
-- The console wraps long lines and a wrap swallows the space it broke at, so
|
||||
-- the announcements below are matched with all whitespace removed.
|
||||
local function textFrom(mark)
|
||||
return table.concat(getLines("main", mark, getLastLineNumber("main") + 1), "")
|
||||
end
|
||||
|
||||
local function containsWrapped(haystack, needle)
|
||||
return contains((tostring(haystack):gsub("%s+", "")), (needle:gsub("%s+", "")))
|
||||
end
|
||||
|
||||
-- A file: URL for a local path, in the three-slash form that keeps a Windows
|
||||
-- drive letter from being read as the host name. The checkout these fixtures
|
||||
-- live in can sit anywhere, so the characters that would otherwise end the path
|
||||
-- early - a space, a fragment, a query, a half-written escape - are encoded.
|
||||
local function fileUrl(path)
|
||||
local normalised = path:gsub("\\", "/"):gsub("[%%#%?%s]", function(character) return string.format("%%%02X", character:byte()) end)
|
||||
if normalised:sub(1, 1) ~= "/" then
|
||||
normalised = "/" .. normalised
|
||||
end
|
||||
return "file://" .. normalised
|
||||
end
|
||||
|
||||
local function copyFile(from, to)
|
||||
local source = io.open(from, "rb")
|
||||
assert.is_not_nil(source, "could not read the fixture " .. from)
|
||||
|
|
@ -926,6 +949,237 @@ describe("Tests installing an archive with nothing in it for Mudlet", function()
|
|||
end)
|
||||
end)
|
||||
|
||||
describe("Tests the functionality of verbosePackageInstall", function()
|
||||
it("installs the package and says so on the main console", function()
|
||||
defer(function() removeFixturePackage(minimalPackage) end)
|
||||
local path = fixtureDirectory .. "/" .. minimalPackage .. ".mpackage"
|
||||
-- an install asked for while a save is running is postponed, and would be
|
||||
-- announced as a success without anything being installed
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
verbosePackageInstall(path)
|
||||
|
||||
assert.is_true(packageInstalled(minimalPackage), "the package was not installed")
|
||||
assert.is_true(containsWrapped(textFrom(mark), "Package '" .. path .. "' installed successfully."), textFrom(mark))
|
||||
end)
|
||||
|
||||
it("says why an install failed", function()
|
||||
-- a path that is not there fails without installing anything, so this spec
|
||||
-- costs none of the profile saves an install-then-reinstall would
|
||||
local path = fixtureDirectory .. "/mudlet-spec-there-is-no-such-package.mpackage"
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
verbosePackageInstall(path)
|
||||
|
||||
local text = textFrom(mark)
|
||||
assert.is_true(containsWrapped(text, "Installing '" .. path .. "' failed:"), text)
|
||||
assert.is_true(containsWrapped(text, "could not open file"), text)
|
||||
assert.is_false(packageInstalled("mudlet-spec-there-is-no-such-package"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Tests the functionality of verboseModuleInstall", function()
|
||||
-- A module is installed from a copy inside the profile for the same reason
|
||||
-- installFixtureModule() does it: a save rewrites a synced module's own
|
||||
-- .mpackage, which must not be the committed fixture.
|
||||
local function stageModule()
|
||||
lfs.mkdir(scratchDirectory)
|
||||
local path = scratchDirectory .. "/" .. moduleName .. ".mpackage"
|
||||
copyFile(fixtureDirectory .. "/" .. moduleName .. ".mpackage", path)
|
||||
return path
|
||||
end
|
||||
|
||||
it("installs the module and says so on the main console", function()
|
||||
defer(function() removeFixtureModule(moduleName) end)
|
||||
local path = stageModule()
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
verboseModuleInstall(path)
|
||||
|
||||
assert.is_true(moduleInstalled(moduleName), "the module was not installed")
|
||||
assert.is_true(containsWrapped(textFrom(mark), "Module '" .. path .. "' installed successfully."), textFrom(mark))
|
||||
end)
|
||||
|
||||
it("says why an install failed", function()
|
||||
local path = fixtureDirectory .. "/mudlet-spec-there-is-no-such-module.mpackage"
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
verboseModuleInstall(path)
|
||||
|
||||
local text = textFrom(mark)
|
||||
-- the module and package failures are announced in the same words, so it is
|
||||
-- the spec above, not this one, that tells the two functions apart
|
||||
assert.is_true(containsWrapped(text, "Installing '" .. path .. "' failed:"), text)
|
||||
assert.is_true(containsWrapped(text, "could not open file"), text)
|
||||
assert.is_false(moduleInstalled("mudlet-spec-there-is-no-such-module"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Tests the functionality of installPackageFromUrl", function()
|
||||
local downloadedName = minimalPackage .. ".mpackage"
|
||||
|
||||
it("downloads the package, installs it and tidies the download away", function()
|
||||
defer(function()
|
||||
removeFixturePackage(minimalPackage)
|
||||
os.remove(getMudletHomeDir() .. "/" .. downloadedName)
|
||||
end)
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
-- a file: URL keeps this off the network while still going through
|
||||
-- downloadFile() and the sysDownloadDone handler the function registers
|
||||
local url = fileUrl(fixtureDirectory .. "/" .. downloadedName)
|
||||
|
||||
installPackageFromUrl(downloadedName, url)
|
||||
|
||||
local event, installedName = waitForEvent("sysInstallPackage", 10000)
|
||||
assert.equals("sysInstallPackage", event)
|
||||
assert.equals(minimalPackage, installedName)
|
||||
assert.is_true(packageInstalled(minimalPackage))
|
||||
local text = textFrom(mark)
|
||||
assert.is_true(containsWrapped(text, "Downloading package from " .. url), text)
|
||||
assert.is_true(containsWrapped(text, "installed successfully."), text)
|
||||
assert.is_false(fileExists(getMudletHomeDir() .. "/" .. downloadedName), "the downloaded copy was left in the profile")
|
||||
end)
|
||||
|
||||
it("names the file, not the whole path, in the announcement", function()
|
||||
-- BUG: verbosePackageInstall() strips the profile folder off the name it
|
||||
-- announces, but uses that folder as a Lua pattern - a profile path holding
|
||||
-- a "-" (a home folder with one will do it) never matches, so the whole
|
||||
-- path is announced instead of the file.
|
||||
pending("verbosePackageInstall() strips the profile folder with an unescaped Lua pattern")
|
||||
defer(function()
|
||||
removeFixturePackage(minimalPackage)
|
||||
os.remove(getMudletHomeDir() .. "/" .. downloadedName)
|
||||
end)
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
installPackageFromUrl(downloadedName, fileUrl(fixtureDirectory .. "/" .. downloadedName))
|
||||
|
||||
waitForEvent("sysInstallPackage", 10000)
|
||||
assert.is_true(containsWrapped(textFrom(mark), "Package '" .. downloadedName .. "' installed successfully."), textFrom(mark))
|
||||
end)
|
||||
|
||||
it("reports a download that failed and installs nothing", function()
|
||||
local missingName = "mudlet-spec-never-downloadable.mpackage"
|
||||
defer(function() os.remove(getMudletHomeDir() .. "/" .. missingName) end)
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
installPackageFromUrl(missingName, fileUrl(fixtureDirectory .. "/" .. missingName))
|
||||
|
||||
local event = waitForEvent("sysDownloadError", 10000)
|
||||
assert.equals("sysDownloadError", event)
|
||||
pumpEvents(200)
|
||||
assert.is_false(packageInstalled("mudlet-spec-never-downloadable"))
|
||||
local text = textFrom(mark)
|
||||
-- the warning only means something paired with the download it reports on
|
||||
assert.is_true(containsWrapped(text, "Downloading package from"), text)
|
||||
assert.is_true(containsWrapped(text, "[ WARN ]"), text)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Tests the functionality of packageDrop", function()
|
||||
it("hands a dropped package file to the installer", function()
|
||||
-- The file is one that is not there: what this is about is that dropping
|
||||
-- reaches the installer with the path that was dropped, and installing for
|
||||
-- real costs two profile saves that verbosePackageInstall's own spec has
|
||||
-- already paid for.
|
||||
local path = fixtureDirectory .. "/mudlet-spec-there-is-no-such-drop.mpackage"
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
-- raised rather than called so that the handler registration in Other.lua
|
||||
-- is what is being tested as well
|
||||
raiseEvent("sysDropEvent", path, "mpackage", 10, 10, "main")
|
||||
|
||||
local text = textFrom(mark)
|
||||
assert.is_true(containsWrapped(text, "Installing '" .. path .. "' failed:"), text)
|
||||
assert.is_false(packageInstalled("mudlet-spec-there-is-no-such-drop"))
|
||||
end)
|
||||
|
||||
it("hands on every kind of file Mudlet installs", function()
|
||||
-- same trick as above, so that narrowing the list of suffixes Mudlet
|
||||
-- accepts cannot go unnoticed
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
|
||||
for _, suffix in ipairs({"xml", "zip", "trigger"}) do
|
||||
local path = fixtureDirectory .. "/mudlet-spec-there-is-no-such-drop." .. suffix
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
packageDrop("sysDropEvent", path, suffix)
|
||||
|
||||
local text = textFrom(mark)
|
||||
assert.is_true(containsWrapped(text, "Installing '" .. path .. "' failed:"), suffix .. ": " .. text)
|
||||
end
|
||||
end)
|
||||
|
||||
it("ignores a file whose type Mudlet does not install", function()
|
||||
-- an install that arrives while a save is running is postponed and would
|
||||
-- land after this spec rather than in it
|
||||
assert.is_true(waitForProfileSaveToPass(), "a profile save was still running")
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
assert.equals(0, select('#', packageDrop("sysDropEvent", fixtureDirectory .. "/" .. minimalPackage .. ".mpackage", "exe")))
|
||||
|
||||
assert.is_false(packageInstalled(minimalPackage))
|
||||
-- an install that was attempted says so either way round, so neither
|
||||
-- announcement having been made is what proves the drop was turned away
|
||||
local text = textFrom(mark)
|
||||
assert.is_false(containsWrapped(text, "installed successfully."), text)
|
||||
assert.is_false(containsWrapped(text, "failed:"), text)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Tests the functionality of packageUrlDrop", function()
|
||||
-- installPackageFromUrl() announces the download while the call is still on
|
||||
-- the stack, so that line on the console separates a drop that was passed on
|
||||
-- from one that was turned away without any waiting. Nothing listens on the
|
||||
-- port below, so the download a passed-on drop starts cannot leave the
|
||||
-- machine.
|
||||
local droppedUrl = "http://127.0.0.1:1/mudlet-spec-dropped.mpackage"
|
||||
|
||||
local function announcedADownload(mark)
|
||||
return containsWrapped(textFrom(mark), "Downloading package from")
|
||||
end
|
||||
|
||||
it("hands a dropped package URL to the downloader", function()
|
||||
defer(function() os.remove(getMudletHomeDir() .. "/mudlet-spec-dropped.mpackage") end)
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
packageUrlDrop("sysDropUrlEvent", droppedUrl, "http")
|
||||
|
||||
assert.is_true(containsWrapped(textFrom(mark), "Downloading package from " .. droppedUrl), textFrom(mark))
|
||||
-- let the refused connection be reported here rather than in a later spec
|
||||
waitForEvent("sysDownloadError", 5000)
|
||||
assert.is_false(packageInstalled("mudlet-spec-dropped"))
|
||||
end)
|
||||
|
||||
it("ignores a URL whose scheme it does not handle", function()
|
||||
-- the scheme is a separate argument from the URL, so the URL is one the
|
||||
-- spec above proved would otherwise be downloaded
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
assert.equals(0, select('#', packageUrlDrop("sysDropUrlEvent", droppedUrl, "ftp")))
|
||||
|
||||
assert.is_false(announcedADownload(mark))
|
||||
end)
|
||||
|
||||
it("does not download a URL that is not a package file", function()
|
||||
-- no save to wait for: a URL with the wrong suffix is handed to the plain
|
||||
-- installer, which gives up on opening it as a file before installing
|
||||
-- anything
|
||||
local mark = getLastLineNumber("main")
|
||||
|
||||
packageUrlDrop("sysDropUrlEvent", "http://127.0.0.1:1/mudlet-spec-not-a-package.txt", "http")
|
||||
|
||||
assert.is_false(announcedADownload(mark))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("The package specs clean up after themselves", function()
|
||||
it("leaves no fixture package, module or folder behind", function()
|
||||
for _, name in ipairs(getPackages()) do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue