Skip to content

Commit d946a9d

Browse files
authored
Merge branch 'main' into main
2 parents 827556a + b04b3bf commit d946a9d

File tree

7 files changed

+247
-15
lines changed

7 files changed

+247
-15
lines changed

.github/workflows/GnuTests.yml

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: GnuTests
22

33
# spell-checker:ignore (abbrev/names) CodeCov gnulib GnuTests Swatinem
44
# spell-checker:ignore (jargon) submodules devel
5-
# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e
5+
# spell-checker:ignore (libs/utils) autopoint chksum dpkg getenforce getlimits gperf lcov libexpect limactl pyinotify setenforce shopt texinfo valgrind libattr libcap taiki-e zstd cpio
66
# spell-checker:ignore (options) Ccodegen Coverflow Cpanic Zpanic
77
# spell-checker:ignore (people) Dawid Dziurla * dawidd dtolnay
88
# spell-checker:ignore (vars) FILESET SUBDIRS XPASS
@@ -31,6 +31,7 @@ env:
3131
TEST_STTY_FULL_SUMMARY_FILE: 'gnu-stty-full-result.json'
3232
TEST_SELINUX_FULL_SUMMARY_FILE: 'selinux-gnu-full-result.json'
3333
TEST_SELINUX_ROOT_FULL_SUMMARY_FILE: 'selinux-root-gnu-full-result.json'
34+
TEST_SMACK_FULL_SUMMARY_FILE: 'smack-gnu-full-result.json'
3435

3536
jobs:
3637
native:
@@ -318,8 +319,52 @@ jobs:
318319
gnu/tests-selinux/*.log
319320
gnu/tests-selinux/*/*.log.gz
320321
322+
smack:
323+
name: Run GNU tests (SMACK)
324+
runs-on: ubuntu-24.04
325+
steps:
326+
- name: Checkout code (uutils)
327+
uses: actions/checkout@v6
328+
with:
329+
path: 'uutils'
330+
persist-credentials: false
331+
- uses: dtolnay/rust-toolchain@master
332+
with:
333+
toolchain: stable
334+
components: rustfmt
335+
- uses: Swatinem/rust-cache@v2
336+
with:
337+
workspaces: "./uutils -> target"
338+
- name: Checkout code (GNU coreutils)
339+
run: (mkdir -p gnu && cd gnu && bash ../uutils/util/fetch-gnu.sh)
340+
- name: Install dependencies
341+
run: |
342+
sudo apt-get update
343+
sudo apt-get install -y qemu-system-x86 zstd cpio
344+
- name: Run GNU SMACK tests
345+
run: |
346+
cd uutils
347+
bash util/run-gnu-tests-smack-ci.sh "$GITHUB_WORKSPACE/gnu" "$GITHUB_WORKSPACE/gnu/tests-smack"
348+
- name: Extract testing info into JSON
349+
run: |
350+
python3 uutils/util/gnu-json-result.py gnu/tests-smack > ${{ env.TEST_SMACK_FULL_SUMMARY_FILE }}
351+
- name: Upload SMACK json results
352+
uses: actions/upload-artifact@v6
353+
with:
354+
name: smack-gnu-full-result
355+
path: ${{ env.TEST_SMACK_FULL_SUMMARY_FILE }}
356+
- name: Compress SMACK test logs
357+
run: gzip gnu/tests-smack/*/*.log 2>/dev/null || true
358+
- name: Upload SMACK test logs
359+
uses: actions/upload-artifact@v6
360+
with:
361+
name: smack-test-logs
362+
path: |
363+
gnu/tests-smack/*.log
364+
gnu/tests-smack/*/*.log.gz
365+
321366
aggregate:
322-
needs: [native, selinux]
367+
needs: [native, selinux, smack]
323368
permissions:
324369
actions: read # for dawidd6/action-download-artifact to query and download artifacts
325370
contents: read # for actions/checkout to fetch code
@@ -384,6 +429,12 @@ jobs:
384429
name: selinux-root-gnu-full-result
385430
path: results
386431
merge-multiple: true
432+
- name: Download smack json results
433+
uses: actions/download-artifact@v7
434+
with:
435+
name: smack-gnu-full-result
436+
path: results
437+
merge-multiple: true
387438
- name: Extract/summarize testing info
388439
id: summary
389440
shell: bash
@@ -394,8 +445,8 @@ jobs:
394445
path_UUTILS='uutils'
395446
396447
json_count=$(ls -l results/*.json | wc -l)
397-
if [[ "$json_count" -ne 5 ]]; then
398-
echo "::error ::Failed to download all results json files (expected 4 files, found $json_count); failing early"
448+
if [[ "$json_count" -ne 6 ]]; then
449+
echo "::error ::Failed to download all results json files (expected 6 files, found $json_count); failing early"
399450
ls -lR results || true
400451
exit 1
401452
fi

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ are some tips for writing good issues:
8787
- What platform are you on?
8888
- Provide a way to reliably reproduce the issue.
8989
- Be as specific as possible!
90+
- Please provide the output with LANG=C, except for locale-related bugs.
9091

9192
### Writing Documentation
9293

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,6 @@ options might be missing or different behavior might be experienced.
3232
We provide prebuilt binaries at https://github.com/uutils/coreutils/releases/latest .
3333
It is recommended to install from main branch if you install from source.
3434

35-
To install it:
36-
37-
```shell
38-
cargo install --git https://github.com/uutils/coreutils coreutils
39-
# cargo install --git https://github.com/uutils/coreutils uu_true # for one util only
40-
~/.cargo/bin/coreutils
41-
```
42-
4335
</div>
4436

4537
<!-- markdownlint-disable-next-line MD026 -->

src/uu/date/src/locale.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,23 @@ cfg_langinfo! {
2929
use std::ffi::CStr;
3030
use std::sync::OnceLock;
3131
use nix::libc;
32+
33+
#[cfg(test)]
34+
use std::sync::Mutex;
3235
}
3336

3437
cfg_langinfo! {
3538
/// Cached locale date/time format string
3639
static DEFAULT_FORMAT_CACHE: OnceLock<&'static str> = OnceLock::new();
3740

41+
/// Mutex to serialize setlocale() calls during tests.
42+
///
43+
/// setlocale() is process-global, so parallel tests that call it can
44+
/// interfere with each other. This mutex ensures only one test accesses
45+
/// locale functions at a time.
46+
#[cfg(test)]
47+
static LOCALE_MUTEX: Mutex<()> = Mutex::new(());
48+
3849
/// Returns the default date format string for the current locale.
3950
///
4051
/// The format respects locale preferences for time display (12-hour vs 24-hour),
@@ -55,6 +66,11 @@ cfg_langinfo! {
5566

5667
/// Retrieves the date/time format string from the system locale
5768
fn get_locale_format_string() -> Option<String> {
69+
// In tests, acquire mutex to prevent race conditions with setlocale()
70+
// which is process-global and not thread-safe
71+
#[cfg(test)]
72+
let _lock = LOCALE_MUTEX.lock().unwrap();
73+
5874
unsafe {
5975
// Set locale from environment variables
6076
libc::setlocale(libc::LC_TIME, c"".as_ptr());
@@ -158,11 +174,24 @@ mod tests {
158174

159175
#[test]
160176
fn test_c_locale_format() {
161-
// Save original locale
177+
// Acquire mutex to prevent interference with other tests
178+
let _lock = LOCALE_MUTEX.lock().unwrap();
179+
180+
// Save original locale (both environment and process locale)
162181
let original_lc_all = std::env::var("LC_ALL").ok();
163182
let original_lc_time = std::env::var("LC_TIME").ok();
164183
let original_lang = std::env::var("LANG").ok();
165184

185+
// Save current process locale
186+
let original_process_locale = unsafe {
187+
let ptr = libc::setlocale(libc::LC_TIME, std::ptr::null());
188+
if ptr.is_null() {
189+
None
190+
} else {
191+
CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_string())
192+
}
193+
};
194+
166195
unsafe {
167196
// Set C locale
168197
std::env::set_var("LC_ALL", "C");
@@ -177,7 +206,7 @@ mod tests {
177206
if d_t_fmt_ptr.is_null() {
178207
None
179208
} else {
180-
std::ffi::CStr::from_ptr(d_t_fmt_ptr).to_str().ok()
209+
CStr::from_ptr(d_t_fmt_ptr).to_str().ok()
181210
}
182211
};
183212

@@ -190,7 +219,7 @@ mod tests {
190219
assert!(uses_24_hour, "C locale should use 24-hour format, got: {locale_format}");
191220
}
192221

193-
// Restore original locale
222+
// Restore original environment variables
194223
unsafe {
195224
if let Some(val) = original_lc_all {
196225
std::env::set_var("LC_ALL", val);
@@ -208,6 +237,17 @@ mod tests {
208237
std::env::remove_var("LANG");
209238
}
210239
}
240+
241+
// Restore original process locale
242+
unsafe {
243+
if let Some(locale) = original_process_locale {
244+
let c_locale = std::ffi::CString::new(locale).unwrap();
245+
libc::setlocale(libc::LC_TIME, c_locale.as_ptr());
246+
} else {
247+
// Restore from environment
248+
libc::setlocale(libc::LC_TIME, c"".as_ptr());
249+
}
250+
}
211251
}
212252

213253
#[test]

src/uucore/src/lib/features/backup_control.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ mod tests {
683683
let result = determine_backup_mode(&matches).unwrap();
684684

685685
assert_eq!(result, BackupMode::Numbered);
686+
unsafe { env::remove_var(ENV_VERSION_CONTROL) };
686687
}
687688

688689
#[test]

tests/by-util/test_tail.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ const FOLLOW_NAME_SHORT_EXP: &str = "follow_name_short.expected";
7676
#[allow(dead_code)]
7777
const FOLLOW_NAME_EXP: &str = "follow_name.expected";
7878

79+
#[cfg(target_vendor = "apple")]
80+
const DEFAULT_SLEEP_INTERVAL_MILLIS: u64 = 1500;
81+
#[cfg(not(target_vendor = "apple"))]
7982
const DEFAULT_SLEEP_INTERVAL_MILLIS: u64 = 1000;
8083

8184
// The binary integer "10000000" is *not* a valid UTF-8 encoding
@@ -1419,6 +1422,9 @@ fn test_retry6() {
14191422
.arg("existing")
14201423
.run_no_wait();
14211424

1425+
#[cfg(target_vendor = "apple")]
1426+
let delay = 1500;
1427+
#[cfg(not(target_vendor = "apple"))]
14221428
let delay = 1000;
14231429
p.make_assertion_with_delay(delay).is_alive();
14241430

util/run-gnu-tests-smack-ci.sh

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#!/bin/bash
2+
# Run GNU SMACK tests in QEMU with SMACK-enabled kernel
3+
# Usage: run-gnu-tests-smack-ci.sh [GNU_DIR] [OUTPUT_DIR]
4+
# spell-checker:ignore rootfs zstd unzstd cpio newc nographic smackfs devtmpfs tmpfs poweroff libm libgcc libpthread libdl librt sysfs rwxat
5+
set -e
6+
7+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8+
REPO_DIR="$(dirname "$SCRIPT_DIR")"
9+
GNU_DIR="${1:-$REPO_DIR/../gnu}"
10+
OUTPUT_DIR="${2:-$REPO_DIR/target/smack-test-results}"
11+
SMACK_DIR="$REPO_DIR/target/smack-test"
12+
13+
echo "Setting up SMACK test environment..."
14+
rm -rf "$SMACK_DIR"
15+
mkdir -p "$SMACK_DIR"/{rootfs/{bin,lib64,proc,sys,dev,tmp,etc,gnu},kernel}
16+
17+
# Download Arch Linux kernel (has SMACK built-in)
18+
if [ ! -f /tmp/arch-vmlinuz ]; then
19+
echo "Downloading Arch Linux kernel..."
20+
MIRROR="https://geo.mirror.pkgbuild.com/core/os/x86_64"
21+
KERNEL_PKG=$(curl -sL "$MIRROR/" | grep -oP 'linux-[0-9][^"]*-x86_64\.pkg\.tar\.zst' | grep -v headers | sort -V | tail -1)
22+
[ -z "$KERNEL_PKG" ] && { echo "Error: Could not find kernel package"; exit 1; }
23+
curl -sL -o /tmp/arch-kernel.pkg.tar.zst "$MIRROR/$KERNEL_PKG"
24+
zstd -d /tmp/arch-kernel.pkg.tar.zst -o /tmp/arch-kernel.pkg.tar 2>/dev/null || unzstd /tmp/arch-kernel.pkg.tar.zst -o /tmp/arch-kernel.pkg.tar
25+
VMLINUZ_PATH=$(tar -tf /tmp/arch-kernel.pkg.tar | grep 'vmlinuz$' | head -1)
26+
tar -xf /tmp/arch-kernel.pkg.tar -C /tmp "$VMLINUZ_PATH"
27+
mv "/tmp/$VMLINUZ_PATH" /tmp/arch-vmlinuz
28+
rm -rf /tmp/usr /tmp/arch-kernel.pkg.tar /tmp/arch-kernel.pkg.tar.zst
29+
fi
30+
cp /tmp/arch-vmlinuz "$SMACK_DIR/kernel/vmlinuz"
31+
32+
# Setup busybox
33+
BUSYBOX=/tmp/busybox
34+
[ -f "$BUSYBOX" ] || curl -sL -o "$BUSYBOX" https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
35+
chmod +x "$BUSYBOX"
36+
cp "$BUSYBOX" "$SMACK_DIR/rootfs/bin/"
37+
(cd "$SMACK_DIR/rootfs/bin" && "$BUSYBOX" --list | xargs -I{} ln -sf busybox {} 2>/dev/null)
38+
39+
# Copy required libraries
40+
for lib in ld-linux-x86-64.so.2 libc.so.6 libm.so.6 libgcc_s.so.1 libpthread.so.0 libdl.so.2 librt.so.1; do
41+
path=$(ldconfig -p | grep "$lib" | head -1 | awk '{print $NF}')
42+
[ -n "$path" ] && [ -f "$path" ] && cp -L "$path" "$SMACK_DIR/rootfs/lib64/" 2>/dev/null || true
43+
done
44+
45+
# Create minimal config files
46+
echo -e "root:x:0:0:root:/root:/bin/sh\nnobody:x:65534:65534:nobody:/nonexistent:/bin/sh" > "$SMACK_DIR/rootfs/etc/passwd"
47+
echo -e "root:x:0:\nnobody:x:65534:" > "$SMACK_DIR/rootfs/etc/group"
48+
touch "$SMACK_DIR/rootfs/etc/mtab"
49+
50+
# Copy GNU tests
51+
cp -r "$GNU_DIR/tests" "$SMACK_DIR/rootfs/gnu/"
52+
53+
# Create init script
54+
cat > "$SMACK_DIR/rootfs/init" << 'INIT'
55+
#!/bin/sh
56+
mount -t proc proc /proc
57+
mount -t sysfs sys /sys
58+
mount -t smackfs smackfs /sys/fs/smackfs 2>/dev/null || true
59+
if [ -d /sys/fs/smackfs ]; then
60+
echo "_" > /proc/self/attr/current 2>/dev/null || true
61+
echo "_ _ rwxat" > /sys/fs/smackfs/load 2>/dev/null || true
62+
fi
63+
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
64+
ln -sf /proc/mounts /etc/mtab
65+
mkdir -p /tmp && mount -t tmpfs tmpfs /tmp
66+
chmod 1777 /tmp
67+
export PATH="/bin:$PATH" srcdir="/gnu" LD_LIBRARY_PATH="/lib64"
68+
cd /gnu/tests
69+
sh "$TEST_SCRIPT"
70+
echo "EXIT:$?"
71+
poweroff -f
72+
INIT
73+
chmod +x "$SMACK_DIR/rootfs/init"
74+
75+
# Build utilities with SMACK support (only ls has SMACK support for now)
76+
# TODO: When other utilities have SMACK support, build: ls id mkdir mknod mkfifo
77+
echo "Building utilities with SMACK support..."
78+
cargo build --release --manifest-path="$REPO_DIR/Cargo.toml" --package uu_ls --bin ls --features uu_ls/smack
79+
80+
# Find SMACK tests
81+
SMACK_TESTS=$(grep -l 'require_smack_' -r "$GNU_DIR/tests/" 2>/dev/null || true)
82+
[ -z "$SMACK_TESTS" ] && { echo "No SMACK tests found"; exit 0; }
83+
84+
echo "Found $(echo "$SMACK_TESTS" | wc -l) SMACK tests"
85+
86+
# Create output directory
87+
rm -rf "$OUTPUT_DIR"
88+
mkdir -p "$OUTPUT_DIR"
89+
90+
# Run each test
91+
for TEST_PATH in $SMACK_TESTS; do
92+
TEST_REL="${TEST_PATH#"$GNU_DIR"/tests/}"
93+
TEST_DIR=$(dirname "$TEST_REL")
94+
TEST_NAME=$(basename "$TEST_REL" .sh)
95+
96+
echo "Running: $TEST_REL"
97+
98+
# Create working copy
99+
WORK="/tmp/smack-test-$$"
100+
rm -rf "$WORK" "$WORK.gz"
101+
cp -a "$SMACK_DIR/rootfs" "$WORK"
102+
103+
# Copy built utilities (only ls has SMACK support for now)
104+
# TODO: When other utilities have SMACK support, use:
105+
# for U in ls id mkdir mknod mkfifo; do cp "$REPO_DIR/target/release/$U" "$WORK/bin/$U"; done
106+
rm -f "$WORK/bin/ls"
107+
cp "$REPO_DIR/target/release/ls" "$WORK/bin/ls"
108+
109+
# Set test script path
110+
sed -i "s|\$TEST_SCRIPT|$TEST_REL|g" "$WORK/init"
111+
112+
# Build initramfs and run
113+
(cd "$WORK" && find . | cpio -o -H newc 2>/dev/null | gzip > "$WORK.gz")
114+
115+
OUTPUT=$(timeout 120 qemu-system-x86_64 \
116+
-kernel "$SMACK_DIR/kernel/vmlinuz" \
117+
-initrd "$WORK.gz" \
118+
-append "console=ttyS0 quiet panic=-1 security=smack lsm=smack" \
119+
-nographic -m 256M -no-reboot 2>&1) || true
120+
121+
# Determine result
122+
if echo "$OUTPUT" | grep -q "EXIT:0"; then
123+
RESULT="PASS"; EXIT_STATUS=0
124+
elif echo "$OUTPUT" | grep -q "EXIT:77"; then
125+
RESULT="SKIP"; EXIT_STATUS=77
126+
else
127+
RESULT="FAIL"; EXIT_STATUS=1
128+
fi
129+
130+
echo " $RESULT: $TEST_REL"
131+
132+
# Create log file for gnu-json-result.py
133+
mkdir -p "$OUTPUT_DIR/$TEST_DIR"
134+
echo "$OUTPUT" > "$OUTPUT_DIR/$TEST_DIR/$TEST_NAME.log"
135+
echo "" >> "$OUTPUT_DIR/$TEST_DIR/$TEST_NAME.log"
136+
echo "$RESULT $TEST_NAME.sh (exit status: $EXIT_STATUS)" >> "$OUTPUT_DIR/$TEST_DIR/$TEST_NAME.log"
137+
138+
rm -rf "$WORK" "$WORK.gz"
139+
done
140+
141+
echo "Done. Results in $OUTPUT_DIR"

0 commit comments

Comments
 (0)