Unable to run test that uses import outside current directory?

Let’s say I have the following dir structure:

src
 |
 +-- root.zig
 |    
 +-- dep
    |  
    +-- dep.zig

In dep.zig I import root.zig for a function declared in there.
If I now run zig test src/dep/dep.zig I get the following

error: import of file outside module path ‘…/root.zig’

This is an oversimplified sample but as far as I can see is that as soon as you have an import outside of the directory of the file you want to test than it will fail.

Is this an issue with my setup or a limitation of the zig test command? And is there a way arround this?

zig build test works fine in this case but sometimes it can be nice to be able to test some code eventhough the overal build would fail because of some unrelated code.

Maybe upper dir notation (..) will fix your problem?

const root = @import("../root.zig");

That’s exactly what I’m doing, this is the code in dep.zig:

const root = @import("../root.zig");

test "test" {
    root.functionToTest();
}

Ok, then the question - why place a test of a thing below a dir where that thing is located?

Its just an oversimplified sample, the same happens with the following code:

const root = @import("../root.zig");

fn functionToTest() void {
    const i = root.functionToTest();

    // do something with i
    _ = i; // autofix
}

test "test" {
    functionToTest();
}

I think this is a somewhat standard usecase right? your code can have dependencies outside the current file, the issue is that as soon as one of those dependencies is outside the same directory it goes wrong.

1 Like
zig-lang/test$ tree
.
├── a
│   └── a.zig
├── b
│   └── b.zig
└── test.zig

2 directories, 3 files

test.zig:

const a = @import("a/a.zig").a;
const b = @import("b/b.zig").b;

test "a and b" {
    a();
    b();
}

a/a.zig:

const std = @import("std");

pub fn a() void {
    std.debug.print("{s}", .{"a"});
}

b/b.zig:

const std = @import("std");

pub fn b() void {
    std.debug.print("{s}", .{"b"});
}
zig-lang/test$ zig test test.zig 
TeAll 1 tests passed.

TeAll? What was that? :slight_smile:

Haha no idea :smiley: , but in your sample the test does not depend on something in a parent dir. lets say you have a test defined in a.zig. and a.zig has a dependency on b.zig. in that case it will fail. And yes you could indeed than define the tests as you did in the parent dir but I prefer to have the tests close to the source otherwise test.zig can become very large. Here a sample using your code:

.
├── a
│   └── a.zig
├── b
│   └── b.zig
└── test.zig

2 directories, 3 files

test.zig

const std = @import("std");

pub const a = @import("a/a.zig");
pub const b = @import("b/b.zig");

test {
    @import("std").testing.refAllDecls(@This());
}

a/a.zig

const std = @import("std");
const b = @import("../b/b.zig");

pub fn a() void {
    b.b();
}

test "test" {
    a();
}

b/b.zig

const std = @import("std");

pub fn b() void {
    std.debug.print("{s}", .{"b"});
}

In this case test.zig is used to execute all tests but I would still like to be able to run the single test in a.zig, unfortunatly that is not possible. But indeed it can be a work around by for example defining tests_a.zig next to tests.zig that contains the a specific tests. I just had hoped that would not be needed.

Off topic:

there was an update yesterday (Linux Mint 21.3)
grep language-pack-en /var/log/dpkg.log
2024-02-16 20:51:46 upgrade language-pack-en:all 1:22.04+20230801 1:22.04+20240212
2024-02-16 20:51:46 status half-configured language-pack-en:all 1:22.04+20230801
2024-02-16 20:51:46 status unpacked language-pack-en:all 1:22.04+20230801
2024-02-16 20:51:46 status half-installed language-pack-en:all 1:22.04+20230801
2024-02-16 20:51:46 status unpacked language-pack-en:all 1:22.04+20240212
2024-02-16 20:51:46 upgrade language-pack-en-base:all 1:22.04+20230801 1:22.04+20240212
2024-02-16 20:51:46 status half-configured language-pack-en-base:all 1:22.04+20230801
2024-02-16 20:51:47 status unpacked language-pack-en-base:all 1:22.04+20230801
2024-02-16 20:51:47 status half-installed language-pack-en-base:all 1:22.04+20230801
2024-02-16 20:51:47 status unpacked language-pack-en-base:all 1:22.04+20240212
2024-02-16 20:51:48 configure language-pack-en:all 1:22.04+20240212 <none>
2024-02-16 20:51:48 status unpacked language-pack-en:all 1:22.04+20240212
2024-02-16 20:51:48 status half-configured language-pack-en:all 1:22.04+20240212
2024-02-16 20:51:48 status installed language-pack-en:all 1:22.04+20240212
2024-02-16 20:51:48 configure language-pack-en-base:all 1:22.04+20240212 <none>
2024-02-16 20:51:48 status unpacked language-pack-en-base:all 1:22.04+20240212
2024-02-16 20:51:48 status half-configured language-pack-en-base:all 1:22.04+20240212
2024-02-16 20:51:49 status installed language-pack-en-base:all 1:22.04+20240212

Maybe this is the reason for that TeAll, one never knows :frowning:

1 Like

The way I see it, in Zig we are sort of building pyramids, the lowest layer has access to everything above it, next layer up has access to everything above it and so on.

Then you can use modules to refer to the base layer of another pyramid.
Basically if something needs to be accessed from multiple files, it should have a common parent directory and be at the same level or below the file that refers to it.

In this analogy, this prevents us from building inverted pyramids where layers higher up get bigger and wider, it is a sort of loose ordering on how things can be organized.

Personally it took a bit to get used to, but now I appreciate that I don’t have to follow crazy redirecting relative imports that send me to crazy places all over the place in that folder structure. One of the good things about it is that folders can be seen as sub units in some sense.

3 Likes

I like this analogy, thanks! Though I am already running into something that I am not sure how to solve in this particular way.

For example, If I would work on some cross platform code to create windows for example. Using you analogy I would have the following file structure:

├── linux
│   └── native_window.zig
├── win32
│   └── native_window.zig
└── window.zig

Here window.zig would contain the general interface that contains a native_window as member for example:

window.zig

const NativeWindow = switch (builtin.os.tag) {
    inline .windows => @import("win32/native_window.zig").NativeWindow,
    inline .linux => @import("linux/native_window.zig").NativeWindow,
    else => {
        @compileError("Platform not supported");
    },
};

pub const Window = struct {
    pub const Mode = enum {
        fullscreen,
        borderless,
        windowed,
    };

    native: NativeWindow,
    mode: Mode,

    pub fn init(mode: Mode) !Window {
        var self = Window{
            .mode = mode
        };
        
        self.native = NativeWindow.init(mode);
    }
};

win32/native_window.zig

pub const NativeWindow = struct {
    pub fn init(mode: Mode) !NativeWindow {
        return NativeWindow{};
    }
};

How would I get access to the enum Mode in my native window? Using your anology I would need to declare Mode in win32/native_window.zig but that would mean I would need to redeclare it for all platforms.

I could remove the platform specific directory and call it win32_window.zig or something but I would like to be able to organize it in some way.

2 Likes

I searched my code base and there is one place where I actually use .. to navigate in a deeper folder structure one level up to refer to a bunch of types that are common to a bunch of different files. I guess there are places where the analogy fails.

I think the problem might be that the zig test command doesn’t seem to have support for being used for projects with more complex imports, I think you need to use a custom build step to define your test run similar to this (but for testing instead of docs) Zig Autodoc : exclude anonymous imports? - #2 by dimdin
Basically I think the zig test command is only for simple cases, for more complex ones define a build step that does the testing.

I don’t quite know how the zig test command determines the root of the module, you also could try calling it from a parent directory, but I am not sure if that works.
Have you tried cd src and the zig test dep/dep.zig so that theoretically the command may have the same root directory?

I just know that I had cases where I gave up trying to use zig test and then just used testing via a build system run step.

1 Like

Why do you want to have platform-specific code in subdirectories? I see it uses non-platform-specific code Mode, so the separation seems broken already? Maybe an interface is something you could consider here? Not sure though if that solves the problem you have in mind.

Concerning testing (and since my question on autodoc was mentioned - which is not related to a directory structure btw.), I really like to configure and call specific tests via steps in the build.zig. That way, you can for example use library code in a test just like an application would use it. A nice side effect is that you can put your tests in separate files in a separate directory :wink:

2 Likes

Consider directory structure like this

.
├── logger
│   └── logger.zig
├── main.zig
├── scanner.zig
├── test
│   └── token_test.zig
├── tokens
│   ├── tokens.zig
│   └── token.zig
└── utils
    └── objects.zig

Now, if i want to access tokens.zig and token.zig in token_test.zig i will end up with “can’t import outside module path” how to handle this.

I am new to zig and trying to understand how this works.

Hello @prashanthsp6498
Welcome to ziggit :slight_smile:

Possible solutions:

  1. Move the test folder into tokens folder and use @import("../token.zig").
  2. Move token_test.zig from test folder to tokens folder and use @import("token.zig").
  3. Move your tests inside token.zig (no need to import anything).
  4. Use createModule to create a token module with .root_source_file = "tokens/token.zig" then you can use @import("token"). See: Importing by module or source file
3 Likes

I feel like zig test should take an argument that is the main.zig/root.zig so it can include items in the same way the build would. The suggestions here feel like a bandaid.
More baindaids, I would use test but that means:

  • Moving test cases to the root folder.
  • Marking items as public that would not typically be pubilc to support test cases.
  • Moving items included from a sibling folder into the same folder, to make some of the test work, or worse making a copy.

I believe that the proper solution would be more akin to zig test accepting a root file:
zig test ./token_test.zig --test_root=main.zig

I’m curious if anyone disagrees.

This program can execute such test as if the file was in the root.
test.zig:

const std = @import("std");

fn replaceBackslashes(path: []u8) void {
    for (path) |*c| {
        if (c.* == '\\') c.* = '/';
    }
}

pub fn main() !void {

    // Get allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    // Parse args into string array (error union needs 'try')
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    // Generate a temporary filename with a `.zig` extension
    const temp_file = "testTemp_zzqqzzq.zig";

    const file_path = try std.fs.cwd().createFile(temp_file, .{});
    defer {
        std.fs.cwd().deleteFile(temp_file) catch unreachable;
        file_path.close();
    }

    replaceBackslashes(args[1]);

    // Write content to the file
    try file_path.writer().print(
        \\comptime {{
        \\_ = @import("{s}");
        \\}}
    ,
        .{args[1]},
    );

    const argv = [_][]const u8{ "zig", "test", temp_file };
    var child = std.process.Child.init(&argv, std.heap.page_allocator);
    try child.spawn();
    _ = try child.wait();
}

It works by generating a temporary file with the item that your interested in testing in a comptime block.

testTemp_zzqqzzq.zig:

comptime {
    _ = @import("./test/token_test.zig");
}

and then runs

zig test ./testTemp_zzqqzzq.zig

which includes the test of interest

it deletes the file when it’s done. this is more of a proof of concept than a polished thing.

to use it just make an exe from the file above and run in the project root.
test.exe ./test/token_test.zig