
Una frustración que tengo con swift es el hecho de tener que pasar por toda la ceremonia de crear un proyecto/package cada vez que quiero hacer una exploración.
XCode es pesado y trae un bagaje que se convierte en ruido en esas etapas de un proyecto, por eso suelo empezar por un fichero y un entorno ligero como CodeRunner o Neovim.
Pero algo que hecho de menos es tener un entorno de testing.
Una solución naive puede ser escribir una librería ligera y copiarla en cada nueva exploración.
// mytest.test.swift
test("some test that will fail") {
assertEqual("a", "b")
}
Import Foundation
struct StandardError: TextOutputStream, Sendable {
private static let handle = FileHandle.standardError
public func write(_ string: String) {
Self.handle.write(Data(string.utf8))
}
}
var stderr = StandardError()
func renderError(file: StaticString, line: UInt, message: String, to stderr: inout StandardError) {
print("\(file):\(line): \(message)", to: &stderr)
}
func assertEqual<Type: Equatable>(_ a: Type, _ b: Type, _ message: String? = nil, file: StaticString = #file, line: UInt = #line) {
if a != b {
renderError(file: file, line: line, message: message ?? "assert equal failed", to: &stderr)
}
}
func test(\_ name: String, action: () -> Void) {
action()
}
Este enfoque tiene varios inconvenientes, entre ellos el hecho de que el código fuente de la mini lib ocupa espacio en el fichero del test en cuestión, añadiendo ruido visual.
También implica tener que copiarla manualmente cada vez que quiera utilizarla, lo que es un dolor, a parte de tener duplicación y tener versiones diferentes si voy añadiendo nuevos asserts y mejorando la librería.
Una solución más apropiada es crear una pequeña librería dinámica a la que enlazar.
La idea es, dado un fichero some.test.swift:
import MiniTests
test("some test that will fail") {
assertEqual("a", "b")
}
Pueda ejecutar directamente en la línea de comandos o en CodeRunner algo como: $test some.
Para ello el flujo sería el siguiente
Compilar la librería a una dylib (y recompilar con cada actualización)
Enlazarla cada vez que queramos ejecutar un *.test.swift
libMiniTests
Compilando a dylib:
mkdir -p ~/.swift_libs/MiniTests
En ~/.swift_libs/MiniTests/build.sh:
swiftc -emit-library -emit-module src.swift \
-module-name MiniTests \
-o libMiniTests.dylib \
-Xlinker -install_name -Xlinker "@rpath/libMiniTests.dylib"
(sin olvidar marcar los métodos assertEqual y test como public)
Damos permisos de ejecución y ejecutamos:
chmod +x build.sh ; ./build.sh
Enlazando al ejecutar un *.test.swift:
swift -I "$HOME/.swift_libs" -L "$HOME/.swift_libs" -lMiniTests "$file"
Alias zsh:
test() {
local base="${1%.test.swift}"
base="${base%.swift}"
local file="${base}.test.swift"
if [[ -f "$file" ]]; then
swift -I "$HOME/.swift_libs" -L "$HOME/.swift_libs" -lMiniTests "$file"
else
echo "File not found: $file"
fi
}
También podemos usar el compilador, útil si estamos usando un debugger, por ejemplo en CodeRunner:
#!/bin/bash
[ -z "$CR_SUGGESTED_OUTPUT_FILE" ] && CR_SUGGESTED_OUTPUT_FILE="$PWD/${CR_FILENAME%.*}"
if [ "$CR_FILENAME" = "main.swift" ]; then
# Fitler out test files
SOURCES=$(ls *.swift | grep -v "\.test\.swift$")
xcrun -sdk macosx swiftc -o "$CR_SUGGESTED_OUTPUT_FILE" $SOURCES "${@:1}" ${CR_DEBUGGING:+-g}
else
# Check if file is a test
case "$CR_FILENAME" in
*.test.swift)
TEST_LIB_PATH="$HOME/.swift_libs/MiniTests"
xcrun -sdk macosx swiftc \
-I "$TEST_LIB_PATH" \
-L "$TEST_LIB_PATH" \
-lMiniTests \
-o "$CR_SUGGESTED_OUTPUT_FILE" \
-Xlinker -rpath -Xlinker "$TEST_LIB_PATH" \
"$CR_FILENAME" "${@:1}" ${CR_DEBUGGING:+-g}
;;
*)
xcrun -sdk macosx swiftc -o "$CR_SUGGESTED_OUTPUT_FILE" "$CR_FILENAME" "${@:1}" ${CR_DEBUGGING:+-g}
;;
esac
fi
status=$?
if [ $status -eq 0 ]; then
echo "$CR_SUGGESTED_OUTPUT_FILE"
fi
exit $status