# utest Module - Unit Testing Framework

The utest module provides a comprehensive unit testing framework for Lua with watch mode support inspired by vitest.

## Basic Usage

```lua
local test = require "utest"

-- Define a test
test.fs.fileExists = function()
    test.is_table(fs)
    local result = fs.fileExists("test.txt")
    test.is_not_nil(result)
end

-- Run all tests and show summary
test.summary()
```

## API Functions

### test.setOption(options)

Configure utest behavior with a table of options.

**Parameters:**

- `options.watch` (boolean) - Enable watch mode
- `options.watch_path` (array) - Optional: additional paths to monitor
- `options.debounce` (number) - Debounce delay in seconds (default: 0.1)
- `options.quiet` (boolean) - Quiet mode (less output)
- `options.grey` (boolean) - Disable colors
- `options.regex` (string) - Run only tests matching Lua pattern

**Returns:** test API (for chaining)

**Example:**

```lua
test.setOption({
    watch = true,
    watch_path = {
        "/path/to/module",
        "/path/to/tests"
    },
    debounce = 0.2
})
```

### test.summary(alreadyWatching)

Run all tests and display summary with coverage information.

**Parameters:**

- `alreadyWatching` (boolean) - Skip watch activation (used internally by watch mode)

**Example:**

```lua
test.summary()
```

### test.watch()

Start watch mode for continuous testing. Monitors file changes and auto-reruns tests.

**Example:**

```lua
test.setOption({watch = true})
-- Define tests...
test.summary()  -- Run tests once, then enter watch mode
```

### test.result()

Get test results count.

**Returns:**

- `testCount` (number) - Total tests run
- `failedCount` (number) - Number of failed tests

**Example:**

```lua
local total, failed = test.result()
print("Ran " .. total .. " tests, " .. failed .. " failed")
```

### test.autoRun()

Convenience function that runs tests normally or in watch mode depending on options.

**Example:**

```lua
test.setOption({watch = true})
test.autoRun()  -- Automatically calls test.summary() or test.watch()
```

## Configuration Options

### Command-Line Style Options

You can pass simple string arguments for quick configuration:

```lua
-- Single options
test.setOption("watch")
test.setOption("quiet")
test.setOption("grey")

-- Multiple options at once
test.setOption("watch quiet")      -- Watch mode + quiet
test.setOption("watch grey quiet") -- All three enabled
test.setOption("no-watch quiet")   -- Disable watch, enable quiet
test.setOption("depth 5")          -- Set monitoring depth
```

**Available string options:**

- `"watch"` - Enable watch mode
- `"no-watch"` - Disable watch mode
- `"quiet"` - Quiet mode (less output)
- `"grey"` - Disable colors
- `"depth N"` - Set monitoring depth (default: 2)

## Assertions

### test.equal(l, r, msg, ...)

Asserts two values are equal.

```lua
test.equal(1 + 1, 2)
test.equal("hello", "hello", "Custom message")
```

### test.not_equal(l, r, msg, ...)

Asserts two values are not equal.

```lua
test.not_equal(1, 2)
```

### test.table_equal(l, r, deep, msg, ...)

Asserts two tables are equal.

```lua
test.table_equal({a = 1}, {a = 1}, true)  -- deep comparison
```

### test.almost_equal(l, r, diff, msg, ...)

Asserts two numbers are almost equal within tolerance.

```lua
test.almost_equal(1.001, 1.0, 0.01)
```

### test.is_true(value, msg, ...)

Asserts value is exactly `true`.

```lua
test.is_true(result)
```

### test.is_false(value, msg, ...)

Asserts value is exactly `false`.

```lua
test.is_false(result)
```

### test.is_nil(value, msg, ...)

Asserts value is nil.

```lua
test.is_nil(invalidValue)
```

### test.is_not_nil(value, msg, ...)

Asserts value is not nil.

```lua
test.is_not_nil(validValue)
```

### test.is_string(value, msg, ...)

### test.is_number(value, msg, ...)

### test.is_table(value, msg, ...)

### test.is_function(value, msg, ...)

### test.is_boolean(value, msg, ...)

### test.is_userdata(value, msg, ...)

### test.is_thread(value, msg, ...)

Type checking assertions.

```lua
test.is_string(name)
test.is_number(count)
test.is_table(options)
```

### test.is_array(value, msg, ...)

Asserts value is an array (sequential table).

```lua
test.is_array(list)
```

### test.is_cdata(value, msg, ...)

Asserts value is cdata (FFI type).

```lua
test.is_cdata(ffiValue)
```

### test.error_raised(f, error_message, msg, ...)

Asserts function raises an error.

```lua
test.error_raised(function() error("test error") end, "test error")
```

### test.errorExpected()

Mark that an error is expected in the test output.

```lua
test.errorExpected()
```

## Test Suites

Organize tests into nested suites using dot notation:

```lua
test.fs.fileExists = function()
    -- Test: fs.fileExists
end

test.fs.readFile = function()
    -- Test: fs.readFile
end

test.string.sub = function()
    -- Test: string.sub
end
```

Suites are automatically created: `test.suiteName.testName`

## Setup and Teardown

Define setup and teardown for test suites:

```lua
test.database.start_up = function()
    -- Runs before each test in database suite
    db.connect()
end

test.database.tear_down = function()
    -- Runs after each test in database suite
    db.close()
end

test.database.insert = function()
    -- Test that uses setup/teardown
end
```

## Coverage Tracking

utest automatically tracks function coverage for tested modules.

### Skip Coverage for Functions

```lua
test.skip_coverage(
    {name = "internalHelper", reason = "internal only"},
    {name = "deprecatedFunc", reason = "deprecated"}
)
```

Coverage report shows:

- Total function calls
- High call functions (>100 calls)
- Function execution time
- Untested functions (with skip reasons)

## Watch Mode

Watch mode uses the `fsevent` module for cross-platform file system monitoring.

### How It Works

1. **Monitors file changes**: Watches for `.lua` file modifications in test directory and module directory
2. **Auto-reruns**: Reruns all tests when changes detected after debounce period
3. **Debouncing**: Waits for configured delay before rerunning (default: 0.1s)
4. **Depth control**: Monitors subdirectories to specified depth (default: 2 levels)

### Configuration

```lua
test.setOption({
    watch = true,
    debounce = 0.2,          -- Wait 0.2s before rerunning
    watch_path = {          -- Additional paths to monitor
        "/path/to/module",
        "/path/to/tests"
    }
})

test.summary()  -- Run tests once, then enter watch mode
```

### Watch Mode Output

```text
[----------] Watch mode enabled
  Monitoring depth 2 for paths: /path/to/test, /path/to/module
Press Ctrl+C to quit

[watch loop 1. 3 files changed, re-running tests]
[watch loop 1. 3 files changed: path1.lua, path2.lua, path3.lua]
```

### fsevent Configuration

**Preset:** `fsevent.preset.safe`

**Event flags:**

- `fileModify`: Enabled
- `fileCloseWrite`: Enabled
- `fileCreate`: Enabled
- `fileDelete`: Disabled
- `fileAccess`: Disabled

**Monitoring:**

- Test file directory (detected automatically)
- Module directory (first `require` path)
- Additional paths from `watch_path` option
- Depth controlled by `depth` option (default: 2)

**Excluded paths:** (handled by fsevent.preset.safe)

## Best Practices

1. **Use descriptive test names**: `test.fs.readFile` clearly indicates what is tested
2. **Group related tests**: Use suites to organize tests by module
3. **Clean up in tear_down**: Ensure tests don't leave artifacts
4. **Use watch mode during development**: Automatically rerun tests on file changes
5. **Disable watch in CI**: Don't enable watch mode in automated tests
6. **Skip internal functions**: Use `skip_coverage` for functions that shouldn't be tested directly
7. **Use type assertions**: `test.is_table`, `test.is_string` catch type errors early

## Example: Complete Test File

```lua
local test = require "utest"
local fs = require "fs"

-- Enable watch mode for development
test.setOption({
    watch = true,
    debounce = 0.2
})

-- Setup/teardown
test.fs.start_up = function()
    -- Create test file
    fs.writeFile("test.tmp", "content")
end

test.fs.tear_down = function()
    -- Clean up
    fs.deleteFile("test.tmp")
end

-- Tests
test.fs.readFile = function()
    local content = fs.readFile("test.tmp")
    test.equal(content, "content")
end

test.fs.writeFile = function()
    local result = fs.writeFile("output.tmp", "data")
    test.is_not_nil(result)
    fs.deleteFile("output.tmp")
end

-- Summary and watch
test.summary()
```

## Command Line Options

utest can be configured via command line arguments when running test files:

- `-h`: Show help message
- `-q`: Quiet mode (less output)
- `-s`: Disable colors
- `pattern`: Run only tests matching Lua pattern

```bash
lua test.lua -q               -- Quiet mode
lua test.lua test.fs          -- Only run fs tests
lua test.lua test.database    -- Only run database tests
```

**Note:** Command-line options set flags before test files are loaded. For watch mode, use `test.setOption({watch = true})` in your test file instead.
