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.

2 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