Skip to content

Commit

Permalink
more stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
zackradisic committed Apr 11, 2024
1 parent e0eca4f commit da35efc
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 29 deletions.
21 changes: 19 additions & 2 deletions src/bun.js/node/node_fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,22 @@ pub const AsyncCpTask = struct {

vtable: AsyncCPTaskVtable = AsyncCPTaskVtable.Dead,

pub fn onCopy(this: *AsyncCpTask, src: [:0]const u8, dest: [:0]const u8) void {
pub fn onCopy(this: *AsyncCpTask, src_: anytype, dest_: anytype) void {
if (@intFromPtr(this.vtable.ctx) == @intFromPtr(AsyncCPTaskVtable.Dead.ctx)) return;
if (comptime bun.Environment.isPosix) return this.vtable.onCopy(this.vtable.ctx, src_, dest_);

var buf: bun.PathBuffer = undefined;
const src: [:0]const u8 = switch (@TypeOf(src_)) {
[:0]const u8, [:0]u8 => src_,
[:0]const u16, [:0]u16 => bun.strings.fromWPath(buf[0..], src_),
else => @compileError("Invalid type: " ++ @typeName(@TypeOf(src_))),
};
const dest: [:0]const u8 = switch (@TypeOf(dest_)) {
[:0]const u8, [:0]u8 => src_,
[:0]const u16, [:0]u16 => bun.strings.fromWPath(buf[0..], dest_),
else => @compileError("Invalid type: " ++ @typeName(@TypeOf(dest_))),
};

this.vtable.onCopy(this.vtable.ctx, src, dest);
}

Expand Down Expand Up @@ -6787,7 +6802,9 @@ pub const NodeFS = struct {
task.finishConcurrently(.{ .err = err });
return false;
},
.result => {},
.result => {
task.onCopy(src, normdest);
},
}

const dir = fd.asDir();
Expand Down
9 changes: 9 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ export const SQL = $cpp("JSSQLStatement.cpp", "createJSSQLStatementConstructor")
export const shellInternals = {
lex: $newZigFunction("shell.zig", "TestingAPIs.shellLex", 1),
parse: $newZigFunction("shell.zig", "TestingAPIs.shellParse", 1),
/**
* Checks if the given builtin is disabled on the current platform
*
* @example
* ```typescript
* const isDisabled = builtinDisabled("cp")
* ```
*/
builtinDisabled: $newZigFunction("shell.zig", "TestingAPIs.disabledOnThisPlatform", 1)
};
34 changes: 24 additions & 10 deletions src/shell/interpreter.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4959,6 +4959,8 @@ pub const Interpreter = struct {
basename,
cp,

pub const DISABLED_ON_POSIX: []const Kind = &.{ .cat, .cp };

pub fn parentType(this: Kind) type {
_ = this;
}
Expand Down Expand Up @@ -4988,13 +4990,15 @@ pub const Interpreter = struct {
}

pub fn fromStr(str: []const u8) ?Builtin.Kind {
if (!bun.Environment.isWindows) {
if (bun.strings.eqlComptime(str, "cat")) {
log("Cat builtin disabled on posix for now", .{});
const result = std.meta.stringToEnum(Builtin.Kind, str) orelse return null;
if (bun.Environment.isWindows) return result;
inline for (Builtin.Kind.DISABLED_ON_POSIX) |disabled| {
if (disabled == result) {
log("{s} builtin disabled on posix for now", .{@tagName(disabled)});
return null;
}
}
return std.meta.stringToEnum(Builtin.Kind, str);
return result;
}
};

Expand Down Expand Up @@ -10304,6 +10308,7 @@ pub const Interpreter = struct {
src_copy: ?[:0]const u8 = null,
tgt_copy: ?[:0]const u8 = null,
cwd_path: [:0]const u8,
verbose_output_lock: std.Thread.Mutex = .{},
verbose_output: ArrayList(u8) = ArrayList(u8).init(bun.default_allocator),
vtable: NodeCpVtable,

Expand Down Expand Up @@ -10382,7 +10387,8 @@ pub const Interpreter = struct {
pub fn isDir(this: *ShellCpTask, path: [:0]const u8) Maybe(bool) {
_ = this;
if (bun.Environment.isWindows) {
const attributes = windows.GetFileAttributesW(path);
var wpath: bun.OSPathBuffer = undefined;
const attributes = windows.GetFileAttributesW(bun.strings.toWPath(wpath[0..], path[0..path.len]));
if (attributes == windows.INVALID_FILE_ATTRIBUTES) {
const err: Syscall.Error = .{
.errno = @intFromEnum(bun.C.SystemErrno.ENOENT),
Expand All @@ -10391,7 +10397,7 @@ pub const Interpreter = struct {
};
return .{ .err = err };
}
return attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0;
return .{ .result = (attributes & windows.FILE_ATTRIBUTE_DIRECTORY) != 0 };
}
const stat = switch (Syscall.lstat(path)) {
.result => |x| x,
Expand Down Expand Up @@ -10468,7 +10474,12 @@ pub const Interpreter = struct {

// Any source directory without -R is an error
if (src_is_dir and !this.opts.recursive) {
const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{src}) catch bun.outOfMemory();
const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{this.src}) catch bun.outOfMemory();
return .{ .custom = errmsg };
}

if (!src_is_dir and bun.strings.eql(src, tgt)) {
const errmsg = std.fmt.allocPrint(bun.default_allocator, "{s} and {s} are identical (not copied)", .{ this.src, this.src }) catch bun.outOfMemory();
return .{ .custom = errmsg };
}

Expand Down Expand Up @@ -10504,14 +10515,14 @@ pub const Interpreter = struct {
} else if (this.operands == 2) {
// source_dir -> new_target_dir
} else {
const errmsg = std.fmt.allocPrint(bun.default_allocator, "directory {s} does not exist", .{tgt}) catch bun.outOfMemory();
const errmsg = std.fmt.allocPrint(bun.default_allocator, "directory {s} does not exist", .{this.tgt}) catch bun.outOfMemory();
return .{ .custom = errmsg };
}
}
// Handle the "3rd synopsis": source_files... -> target
else {
if (src_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{src}) catch bun.outOfMemory() };
if (!tgt_exists or !tgt_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is not a directory", .{tgt}) catch bun.outOfMemory() };
if (src_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is a directory (not copied)", .{this.src}) catch bun.outOfMemory() };
if (!tgt_exists or !tgt_is_dir) return .{ .custom = std.fmt.allocPrint(bun.default_allocator, "{s} is not a directory", .{this.tgt}) catch bun.outOfMemory() };
const basename = ResolvePath.basename(src[0..src.len]);
const parts: []const []const u8 = &.{
tgt[0..tgt.len],
Expand Down Expand Up @@ -10571,6 +10582,9 @@ pub const Interpreter = struct {

pub fn onCopy(this: *NodeCpVtable, src: [:0]const u8, dest: [:0]const u8) void {
if (!this.shellTask().opts.verbose) return;
log("onCopy: {s} -> {s}\n", .{ src, dest });
this.shellTask().verbose_output_lock.lock();
defer this.shellTask().verbose_output_lock.unlock();
var writer = this.shellTask().verbose_output.writer();
writer.print("{s} -> {s}\n", .{ src, dest }) catch bun.outOfMemory();
}
Expand Down
23 changes: 23 additions & 0 deletions src/shell/shell.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4323,6 +4323,29 @@ pub fn SmolList(comptime T: type, comptime INLINED_MAX: comptime_int) type {

/// Used in JS tests, see `internal-for-testing.ts` and shell tests.
pub const TestingAPIs = struct {
pub fn disabledOnThisPlatform(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue {
if (comptime bun.Environment.isWindows) return JSValue.false;

const arguments_ = callframe.arguments(1);
var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice());
const string = arguments.nextEat() orelse {
globalThis.throw("shellInternals.disabledOnPosix: expected 1 arguments, got 0", .{});
return JSC.JSValue.jsUndefined();
};

const bunstr = string.toBunString(globalThis);
defer bunstr.deref();
const utf8str = bunstr.toUTF8(bun.default_allocator);
defer utf8str.deinit();

inline for (Interpreter.Builtin.Kind.DISABLED_ON_POSIX) |disabled| {
if (bun.strings.eqlComptime(utf8str.byteSlice(), @tagName(disabled))) {
return JSValue.true;
}
}
return JSValue.false;
}

pub fn shellLex(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
Expand Down
120 changes: 108 additions & 12 deletions test/js/bun/shell/commands/cp.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,143 @@
import { $ } from "bun";
import { TestBuilder } from "../test_builder";
import { beforeAll, describe, test, expect } from "bun:test";
import { beforeAll, describe, test, expect, beforeEach } from "bun:test";
import { sortedShellOutput } from "../util";
import { tempDirWithFiles } from "harness";
import fs from "fs";
import { shellInternals } from "bun:internal-for-testing";
const { builtinDisabled } = shellInternals

describe("bunshell cp", async () => {
describe.if(!builtinDisabled("cp"))("bunshell cp", async () => {
TestBuilder.command`cat ${import.meta.filename} > lmao.txt; cp -v lmao.txt lmao2.txt`
.stdout("$TEMP_DIR/lmao.txt -> $TEMP_DIR/lmao2.txt\n")
.ensureTempDir()
.fileEquals("lmao2.txt", await $`cat ${import.meta.filename}`.text())
.runAsTest('file -> file');
.runAsTest("file -> file");

TestBuilder.command`cat ${import.meta.filename} > lmao.txt; touch lmao2.txt; cp -v lmao.txt lmao2.txt`
.stdout("$TEMP_DIR/lmao.txt -> $TEMP_DIR/lmao2.txt\n")
.ensureTempDir()
.fileEquals("lmao2.txt", await $`cat ${import.meta.filename}`.text())
.runAsTest('file -> existing file replaces contents');
.runAsTest("file -> existing file replaces contents");

TestBuilder.command`cat ${import.meta.filename} > lmao.txt; mkdir lmao2; cp -v lmao.txt lmao2`
.ensureTempDir()
.stdout("$TEMP_DIR/lmao.txt -> $TEMP_DIR/lmao2/lmao.txt\n")
.fileEquals("lmao2/lmao.txt", await $`cat ${import.meta.filename}`.text())
.runAsTest('file -> dir');
.runAsTest("file -> dir");

TestBuilder.command`cat ${import.meta.filename} > lmao.txt; cp -v lmao.txt lmao2/`
.ensureTempDir()
.stderr('cp: $TEMP_DIR/lmao2/ is not a directory\n')
.stderr("cp: lmao2/ is not a directory\n")
.exitCode(1)
.runAsTest('file -> non-existent dir fails');
.runAsTest("file -> non-existent dir fails");

TestBuilder.command`cat ${import.meta.filename} > lmao.txt; cat ${import.meta.filename} > lmao2.txt; mkdir lmao3; cp -v lmao.txt lmao2.txt lmao3`
.ensureTempDir()
.stdout(expectSortedOutput("$TEMP_DIR/lmao.txt -> $TEMP_DIR/lmao3/lmao.txt\n$TEMP_DIR/lmao2.txt -> $TEMP_DIR/lmao3/lmao2.txt\n"))
.stdout(
expectSortedOutput(
"$TEMP_DIR/lmao.txt -> $TEMP_DIR/lmao3/lmao.txt\n$TEMP_DIR/lmao2.txt -> $TEMP_DIR/lmao3/lmao2.txt\n",
),
)
.fileEquals("lmao3/lmao.txt", await $`cat ${import.meta.filename}`.text())
.fileEquals("lmao3/lmao2.txt", await $`cat ${import.meta.filename}`.text())
.runAsTest('file+ -> dir');
.runAsTest("file+ -> dir");

TestBuilder.command`mkdir lmao; mkdir lmao2; cp -v lmao lmao2 lmao3`
.ensureTempDir()
.stderr(expectSortedOutput('cp: $TEMP_DIR/lmao is a directory (not copied)\ncp: $TEMP_DIR/lmao2 is a directory (not copied)\n'))
.stderr(expectSortedOutput("cp: lmao is a directory (not copied)\ncp: lmao2 is a directory (not copied)\n"))
.exitCode(1)
.runAsTest('dir -> ? fails without -R');
.runAsTest("dir -> ? fails without -R");

describe("uutils ported", () => {
const TEST_EXISTING_FILE: string = "existing_file.txt";
const TEST_HELLO_WORLD_SOURCE: string = "hello_world.txt";
const TEST_HELLO_WORLD_SOURCE_SYMLINK: string = "hello_world.txt.link";
const TEST_HELLO_WORLD_DEST: string = "copy_of_hello_world.txt";
const TEST_HELLO_WORLD_DEST_SYMLINK: string = "copy_of_hello_world.txt.link";
const TEST_HOW_ARE_YOU_SOURCE: string = "how_are_you.txt";
const TEST_HOW_ARE_YOU_DEST: string = "hello_dir/how_are_you.txt";
const TEST_COPY_TO_FOLDER: string = "hello_dir/";
const TEST_COPY_TO_FOLDER_FILE: string = "hello_dir/hello_world.txt";
const TEST_COPY_FROM_FOLDER: string = "hello_dir_with_file/";
const TEST_COPY_FROM_FOLDER_FILE: string = "hello_dir_with_file/hello_world.txt";
const TEST_COPY_TO_FOLDER_NEW: string = "hello_dir_new";
const TEST_COPY_TO_FOLDER_NEW_FILE: string = "hello_dir_new/hello_world.txt";

// beforeAll doesn't work beacuse of the way TestBuilder is setup
const tmpdir: string = tempDirWithFiles("cp-uutils", {
"hello_world.txt": "Hello, World!",
"existing_file.txt": "Cogito ergo sum.",
"how_are_you.txt": "How are you?",
"hello_dir": {
"hello.txt": "",
},
"hello_dir_with_file": {
"hello_world.txt": "Hello, World!",
},
"dir_with_10_files": {
"0": "",
"1": "",
"2": "",
"3": "",
"4": "",
"5": "",
"6": "",
"7": "",
"8": "",
"9": "",
},
});

TestBuilder.command`cp ${TEST_HELLO_WORLD_SOURCE} ${TEST_HELLO_WORLD_DEST}`
.ensureTempDir(tmpdir)
.fileEquals(TEST_HELLO_WORLD_DEST, "Hello, World!")
.runAsTest("cp_cp");

TestBuilder.command`cp ${TEST_HELLO_WORLD_SOURCE} ${TEST_EXISTING_FILE}`
.ensureTempDir(tmpdir)
.fileEquals(TEST_EXISTING_FILE, "Hello, World!")
.runAsTest("cp_existing_target");

TestBuilder.command`cp ${TEST_HELLO_WORLD_SOURCE} ${TEST_HELLO_WORLD_SOURCE} ${TEST_COPY_TO_FOLDER}`
.ensureTempDir(tmpdir)
.file(TEST_EXISTING_FILE, "Hello, World!\n")
.runAsTest("cp_duplicate_files");

TestBuilder.command`touch a; cp a a`
.ensureTempDir(tmpdir)
.stderr_contains("cp: a and a are identical (not copied)\n")
.exitCode(1)
.runAsTest("cp_same_file");

TestBuilder.command`cp ${TEST_HELLO_WORLD_SOURCE} ${TEST_HELLO_WORLD_SOURCE} ${TEST_EXISTING_FILE}`
.ensureTempDir(tmpdir)
.stderr_contains(`cp: ${TEST_EXISTING_FILE} is not a directory\n`)
.exitCode(1)
.runAsTest("cp_multiple_files_target_is_file");

TestBuilder.command`cp ${TEST_COPY_TO_FOLDER} ${TEST_HELLO_WORLD_DEST}`
.ensureTempDir(tmpdir)
.stderr_contains(`cp: ${TEST_COPY_TO_FOLDER} is a directory (not copied)\n`)
.exitCode(1)
.runAsTest("cp_directory_not_recursive");

TestBuilder.command`cp ${TEST_HELLO_WORLD_SOURCE} ${TEST_HOW_ARE_YOU_SOURCE} ${TEST_COPY_TO_FOLDER}`
.ensureTempDir(tmpdir)
.fileEquals(TEST_COPY_TO_FOLDER_FILE, "Hello, World!")
.fileEquals(TEST_HOW_ARE_YOU_DEST, "How are you?")
.runAsTest("cp_multiple_files");

TestBuilder.command`cp -R ${TEST_COPY_FROM_FOLDER} ${TEST_COPY_TO_FOLDER_NEW}`
.ensureTempDir(tmpdir)
.fileEquals(TEST_COPY_TO_FOLDER_NEW_FILE, "Hello, World!")
.runAsTest("cp_recurse");
});
});

function expectSortedOutput(expected: string) {
return (stdout: string, tempdir: string) => expect(sortedShellOutput(stdout).join('\n')).toEqual(sortedShellOutput(expected).join('\n').replaceAll("$TEMP_DIR", tempdir))
return (stdout: string, tempdir: string) =>
expect(sortedShellOutput(stdout).join("\n")).toEqual(
sortedShellOutput(expected).join("\n").replaceAll("$TEMP_DIR", tempdir),
);
}
Loading

0 comments on commit da35efc

Please sign in to comment.