클라우드 Mac에서 명령줄 프로그램을 빌드한 뒤 놓치기 쉬운 것은 컴파일 오류보다 호출 계약이다. 도움말이 데이터 스트림에 섞이거나, 실패했는데도 종료 코드가 0이거나, 개발자의 현재 디렉터리에서만 도구가 실행될 수 있다. 아래에서는 작은 Swift 도구로 배포 전 인수 테스트를 구성한다. 예제는 그래픽 세션에 의존하지 않아 무인 빌드 작업에도 사용할 수 있다.
호출 계약부터 정의하기
예제 도구 taskprobe는 --output과 텍스트를 받아 지정한 파일에 텍스트를 쓴다. 파일 경로는 필수 인수다. 도움말 요청은 성공으로 종료하고, 인수가 잘못되면 실패로 종료한다. 기능을 구현하기 전에 호출 측이 결과를 판단할 기준부터 정한다.
| 상황 | 표준 출력 | 표준 오류 | 종료 코드 | 파일 |
|---|---|---|---|---|
--help |
사용법 | 비어 있음 | 0 | 생성하지 않음 |
| 인수 누락 | 비어 있음 | 오류 설명 | 0이 아님 | 생성하지 않음 |
| 정상적으로 쓰기 완료 | 완료 표시 | 비어 있음 | 0 | 내용이 온전함 |
| 쓸 수 없는 경로 | 비어 있음 | 오류 설명 | 0이 아님 | 대상 파일을 생성하지 않음 |
표준 출력에는 기계가 처리할 수 있는 성공 결과만, 표준 오류에는 진단 정보만 보낸다. 호출 스크립트는 종료 코드와 예상 파일을 모두 확인해야 한다. 로그에 “완료”가 있다는 이유만으로 성공으로 판단해서는 안 된다.
원자적 쓰기는 대상 파일이 새 내용의 일부로 덮이는 일을 막지만, 정전 후에도 데이터가 반드시 유지된다는 뜻은 아니다. 영속성이 필요하다면 디스크 기록과 복구 전략을 별도로 설계해야 한다.
테스트할 수 있는 최소 도구 빌드하기
별도 디렉터리에 소스 코드를 저장하고 빌드한다. 프로그램에 출력 위치를 고정하지 않고 호출자가 절대 경로를 전달하게 하면, 테스트 중 작업 디렉터리를 의도적으로 바꿀 수 있다. Foundation의 원자적 쓰기 옵션을 사용해 정상적인 쓰기 과정에서 대상 경로에 부분적인 내용이 노출되지 않도록 한다.
WORK="$(mktemp -d)"
mkdir -p "$WORK/bin" "$WORK/home"
cat > "$WORK/main.swift" <<'SWIFT'
import Foundation
func fail(_ message: String) -> Never {
FileHandle.standardError.write(Data((message + "\n").utf8))
exit(2)
}
let args = Array(CommandLine.arguments.dropFirst())
if args == ["--help"] {
print("usage: taskprobe --output PATH TEXT")
exit(0)
}
guard args.count == 3, args[0] == "--output",
!args[1].isEmpty else {
fail("expected --output PATH TEXT")
}
let target = URL(fileURLWithPath: args[1])
do {
try Data(args[2].utf8).write(to: target, options: .atomic)
print("written")
} catch {
fail("cannot write output: \(error.localizedDescription)")
}
SWIFT
xcrun swiftc -O "$WORK/main.swift" -o "$WORK/bin/taskprobe"
xcrun은 현재 선택된 개발 도구 체인을 사용한다. 빌드 전에 xcode-select -p로 선택 상태를 확인할 수 있지만, 이것이 대상 macOS 버전에서 직접 실행해 보는 과정을 대신하지는 않는다. 인수 테스트는 실제로 배포할 바로 그 바이너리 파일을 대상으로 해야 한다. 테스트를 마친 뒤 다시 빌드한 다른 결과물을 배포하지 않는다.
독립된 디렉터리에서 성공과 실패 검증하기
다음 스크립트는 앞선 셸 세션의 WORK를 이어서 사용한다. 각 검사는 두 출력 스트림을 별도로 저장한다. 또한 테스트에서는 임시 작업 디렉터리로 이동하고 상속된 환경 변수 대부분을 제거한다. 배포할 도구에 특정 변수가 꼭 필요하다면 대화형 셸에서 우연히 제공되기를 기대하지 말고 호출 계약에 명시해야 한다.
set -eu
BIN="$WORK/bin/taskprobe"
cd "$WORK/home"
env -i PATH=/usr/bin:/bin HOME="$WORK/home" \
"$BIN" --help >"$WORK/help.out" 2>"$WORK/help.err"
grep -Fx 'usage: taskprobe --output PATH TEXT' "$WORK/help.out"
test ! -s "$WORK/help.err"
if "$BIN" --output >"$WORK/bad.out" 2>"$WORK/bad.err"; then
echo "missing arguments returned success" >&2
exit 1
fi
test ! -s "$WORK/bad.out"
grep -F 'expected --output' "$WORK/bad.err"
"$BIN" --output "$WORK/home/result.txt" 'hello 云端 Mac' \
>"$WORK/good.out" 2>"$WORK/good.err"
grep -Fx 'written' "$WORK/good.out"
test ! -s "$WORK/good.err"
grep -Fx 'hello 云端 Mac' "$WORK/home/result.txt"
if "$BIN" --output "$WORK/home/absent/result.txt" test \
>"$WORK/fail.out" 2>"$WORK/fail.err"; then
echo "unwritable path returned success" >&2
exit 1
fi
test ! -e "$WORK/home/absent/result.txt"
test ! -s "$WORK/fail.out"
test -s "$WORK/fail.err"
첫 번째 실패 사례는 인수 오류를, 두 번째는 쓰기 오류를 검증한다. chmod로 “쓸 수 없는 디렉터리”를 만들지는 말자. 테스트 프로세스에 특수 권한이 있으면 결과가 달라질 수 있다. 존재하지 않는 상위 디렉터리를 쓰는 편이 더 직접적이지만, 이 역시 I/O 실패의 한 종류만 다룰 뿐 모든 디스크 오류가 예상대로 처리됨을 증명하지는 못한다.
흔한 거짓 양성 세 가지 피하기
파이프라인에 가려지는 종료 코드
taskprobe ... | tee output.log의 종료 코드는 대개 도구 자체가 아니라 파이프라인 마지막 명령에서 나온다. 인수 테스트 스크립트에서는 두 스트림을 직접 리디렉션한 뒤 명령의 종료 상태를 확인하는 편이 좋다. 파이프라인이 꼭 필요하다면 해당 옵션을 지원하는 셸에서 pipefail을 켜고 사용한 셸을 기록한다.
우연히 맞아떨어진 작업 디렉터리
상대 경로는 프로그램을 시작한 디렉터리의 영향을 받는다. 위 테스트가 home에서 바이너리를 실행하고 절대 출력 경로를 전달하는 이유는 숨은 작업 디렉터리 가정을 찾아내기 위해서다. 실제 도구가 패키지에 포함된 리소스를 읽어야 한다면 명시된 리소스 위치를 기준으로 찾아야 하며, 작업이 항상 소스 루트에서 시작한다고 가정해서는 안 된다.
오류 메시지를 데이터로 취급하기
“명령이 무언가 출력했다”는 사실만 확인해서는 의미가 없다. 후속 작업이 표준 출력을 결과 파일에 그대로 저장할 수도 있다. 인수 테스트에서는 실패 시 표준 출력이 비어 있고 표준 오류에는 내용이 있으며 종료 코드는 0이 아님을 함께 확인해야 한다. 성공 시에는 표준 오류가 비어 있는지와 생성된 파일의 실제 내용도 확인한다. 도구가 구조화된 데이터를 출력한다면 파일 크기만 비교하지 말고 형식까지 검증해야 한다.
배포 전 경계 조건 다시 확인하기
인수 테스트 스크립트를 빌드 결과물 옆에 두고 같은 빌드 작업에서 실행한다. 통과한 뒤에는 후속 인계 시 대조할 수 있도록 바이너리의 체크섬과 선택된 도구 체인 정보를 기록한다. 실행 계정은 실제 작업 계정과 동일한 범위의 디렉터리에 접근할 수 있어야 한다. 파일 테스트에는 일회용 디렉터리를 사용해 오래된 결과물 때문에 test -e가 뜻하지 않게 통과하지 않도록 한다.
이 글의 테스트는 정전, 디스크 공간 부족, 다른 프로세스의 대상 파일 동시 변경을 시뮬레이션하지 못하며, 테스트하지 않은 시스템 버전에서 프로그램이 실행됨을 증명하지도 못한다. 이런 보장이 필요하다면 각각 장애 주입, 동시 접근 조정, 대상 시스템에서의 인수 테스트를 추가한다. 임대 기간이 끝나기 전에 소스 코드, 스크립트, 테스트 기록을 이전하고 인계받는 사람이 새 환경에서 전체 검사를 다시 실행해야 한다. “예전에 통과했다”는 새 머신의 인수 테스트 결과가 아니다.
자주 묻는 질문
오류를 표준 출력에 기록하면 왜 문제가 되나요?
호출 스크립트가 표준 출력을 파일로 저장할 수 있습니다. 오류는 표준 오류에 기록하고 0이 아닌 종료 코드를 반환해야 결과 데이터와 구분됩니다.
원자적 쓰기는 테스트 스크립트만으로 보장할 수 있나요?
아니요. 스크립트는 성공과 실패 후의 파일 상태만 확인합니다. 임시 파일과 대상 파일이 같은 파일 시스템에 있는지도 구현에서 보장해야 합니다.
클라우드 Mac을 단독으로 사용하고 싶으신가요?
모델, 리전, 과금 기간을 선택하세요. 전용 물리 머신을 제공하며, 실제 사용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.