diff --git a/.gitignore b/.gitignore
index ea73308..b0e8d5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 _site/
+.luarocks/
 
 # Created by https://www.toptal.com/developers/gitignore/api/git
 # Edit at https://www.toptal.com/developers/gitignore?templates=git
diff --git a/.tests/strings/str_padend.test.lua b/.tests/strings/str_padend.test.lua
new file mode 100644
index 0000000..d710ad7
--- /dev/null
+++ b/.tests/strings/str_padend.test.lua
@@ -0,0 +1,42 @@
+expose("", function()
+	local polyfill = require("worldeditadditions.utils.strings.polyfill")
+	
+	describe("str_padend", function()
+		it("should pad a string", function()
+			assert.are.equal(
+				polyfill.str_padend("test", 5, " "),
+				"test "
+			)
+		end)
+		it("should pad a different string", function()
+			assert.are.equal(
+				polyfill.str_padend("yay", 4, " "),
+				"yay "
+			)
+		end)
+		it("should pad a string with multiple characters", function()
+			assert.are.equal(
+				polyfill.str_padend("test", 10, " "),
+				"test      "
+			)
+		end)
+		it("should not pad a long string", function()
+			assert.are.equal(
+				polyfill.str_padend("testtest", 5, " "),
+				"testtest"
+			)
+		end)
+		it("should pad with other characters", function()
+			assert.are.equal(
+				polyfill.str_padend("1", 2, "0"),
+				"10"
+			)
+		end)
+		it("should pad with multiple other characters", function()
+			assert.are.equal(
+				polyfill.str_padend("1", 5, "0"),
+				"10000"
+			)
+		end)
+	end)
+end)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ca8728..8ed13af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,7 @@ Note to self: See the bottom of this file for the release template text.
      - Any `<chance>` can now either be a 1-in-N number (e.g. `4`, `10`), or a percentage chance (e.g. `50%`, `10%`).
      - Caveat: Percentages are converted to a 1-in-N chance, but additionally that number is rounded down in some places
  - `//torus`, `//hollowtorus`: Add optional new axes
- - `//torus`: Add optional hollow keyword
+ - `//torus`: Add optional hollow keyword - @VorTechnix
  - `//multi`: Add curly brace syntax for nesting command calls ([more information](https://github.com/sbrl/Minetest-WorldEditAdditions/blob/main/Chat-Command-Reference.md#multi-command_a-command_b-command_c-))
  - `//erode`: Add new `river` erosion algorithm for filling in potholes and removing pillars
 
diff --git a/tests.sh b/tests.sh
new file mode 100755
index 0000000..35bac85
--- /dev/null
+++ b/tests.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# Make sure the current directory is the location of this script to simplify matters
+cd "$(dirname "$(readlink -f "$0")")" || { echo "Error: Failed to cd to script directory" >&2; exit 1; };
+
+###############################################################################
+
+log_msg() {
+	echo "[ $SECONDS ] >>> $*" >&2;
+}
+
+# $1 - Command name to check for
+check_command() {
+	set +e;
+	which $1 >/dev/null 2>&1; exit_code=$?
+	if [[ "${exit_code}" -ne 0 ]]; then
+		log_msg "Error: Couldn't locate $1. Make sure it's installed and in your path.";
+	fi
+	set -e;
+}
+
+###############################################################################
+
+check_command luarocks;
+
+luarocks_root="${PWD}/.luarocks";
+
+# Setup the lua module path
+eval "$(luarocks --tree "${luarocks_root}" path)";
+
+mode="${1}"; shift;
+
+run_setup() {
+	log_msg "Installing busted";
+	
+	luarocks --tree "${luarocks_root}" install busted;
+}
+
+run_test() {
+	.luarocks/bin/busted --no-auto-insulate --pattern ".test.lua" .tests;
+}
+
+case "${mode}" in
+	setup )
+		run_setup;
+		;;
+	
+	run )
+		if [[ ! -d "${luarocks_root}" ]]; then
+			run_setup;
+		fi
+		run_test;
+		;;
+	
+	busted )
+		.luarocks/bin/busted "${@}";
+		;;
+	
+	* )
+		echo -e "Usage:
+	path/to/run.sh setup	# Setup to run the tests
+	path/to/run.sh run		# Run the tests" >&2;
+		;;
+esac
diff --git a/worldeditadditions/utils/strings/polyfill.lua b/worldeditadditions/utils/strings/polyfill.lua
index d08edc3..eb3d5af 100644
--- a/worldeditadditions/utils/strings/polyfill.lua
+++ b/worldeditadditions/utils/strings/polyfill.lua
@@ -7,13 +7,13 @@ this?". If yes, then your implementation probably belongs here.
 
 --- Pads str to length len with char from right
 -- @source https://snipplr.com/view/13092/strlpad--pad-string-to-the-left
-function worldeditadditions.str_padend(str, len, char)
+local function str_padend(str, len, char)
 	if char == nil then char = ' ' end
 	return str .. string.rep(char, len - #str)
 end
 --- Pads str to length len with char from left
 -- Adapted from the above
-function worldeditadditions.str_padstart(str, len, char)
+local function str_padstart(str, len, char)
 	if char == nil then char = ' ' end
 	return string.rep(char, len - #str) .. str
 end
@@ -22,7 +22,7 @@ end
 -- @param	str		string	The string to operate on
 -- @param	start	number	The start string to look for
 -- @returns	bool	Whether start is present at the beginning of str
-function worldeditadditions.str_starts(str, start)
+local function str_starts(str, start)
    return string.sub(str, 1, string.len(start)) == start
 end
 
@@ -30,6 +30,18 @@ end
 -- From http://lua-users.org/wiki/StringTrim
 -- @param	str		string	The string to trim the whitespace from.
 -- @returns	string	A copy of the original string with the whitespace trimmed.
-function worldeditadditions.trim(str)
+local function trim(str)
    return (str:gsub("^%s*(.-)%s*$", "%1"))
 end
+
+
+if worldeditadditions then
+	worldeditadditions.str_padend = str_padend
+	worldeditadditions.str_padstart = str_padstart
+	worldeditadditions.str_starts = str_starts
+	worldeditadditions.trim = trim
+else
+    return {
+        str_padend = str_padend
+    }
+end
