3 Commits
Author SHA1 Message Date
Nanaloveyuki cd358f8787 🔖 release 0.8.1 2026-08-25 16:39:49 +08:00
NanaloveyukiandGitHub 3e1c93cc86 🧪 add property-based tests
Add deterministic QuickCheck properties for bounded history retention, queued overflow/drain behavior, patch composition, and logger config round-trips. Local verification passed across native, wasm, wasm-gc, and js targets; GitHub Linux/macOS jobs were blocked by Moon registry network failures during Update Moon registry.
2026-08-25 16:35:42 +08:00
NanaloveyukiandGitHub b4d69675fd ⬆️ upgrade async and replace native file FFI
升级 async 依赖,迁移到 MoonBit 原生 async/fs 文件接口,移除项目自有 C FFI,并固定文本文件为 LF。CI 全部通过。
2026-08-25 16:06:37 +08:00
32 changed files with 1288 additions and 206 deletions
+2
View File
@@ -0,0 +1,2 @@
# Normalize repository text files and check them out with LF on every platform.
* text=auto eol=lf
+1 -1
View File
@@ -17,7 +17,7 @@ BitLogger 的文档按使用路径组织:先完成一个可运行的日志输
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.8.0
moon add Nanaloveyuki/BitLogger@0.8.1
```
### 2. 写入第一条结构化日志
+1 -1
View File
@@ -18,7 +18,7 @@ The documentation is organized around complete usage flows. Start with an execut
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.8.0
moon add Nanaloveyuki/BitLogger@0.8.1
```
### 2. Import And Log
+65
View File
@@ -0,0 +1,65 @@
---
name: file-sink-async
group: api
category: sink
update-time: 20260825
description: Create a file sink from an async context using the native async filesystem interface.
key-word:
- file
- sink
- async
- public
---
## File-sink-async
Create a `FileSink` from an async context. The constructor opens the file with the native `moonbitlang/async/fs` interface and keeps the same rotation, policy, and failure-counter surface as `file_sink(...)`.
### Interface
```moonbit
pub async fn file_sink_async(
path : String,
append~ : Bool = true,
auto_flush~ : Bool = true,
rotation~ : FileRotation? = None,
formatter~ : RecordFormatter = fn(rec) { format_text(rec) },
) -> FileSink {}
```
#### input
- `path : String` - Destination file path.
- `append : Bool` - Whether writes append rather than truncate on the first open.
- `auto_flush : Bool` - Whether each async write requests an async file sync.
- `rotation : FileRotation?` - Optional size-based rotation policy.
- `formatter : RecordFormatter` - Formatter used to render each record.
#### output
- `FileSink` - A file sink ready for use by an async logger or direct async sink writes.
### Explanation
- Use this constructor when the caller is already running inside an async event loop.
- File writes, flushes, file-size checks, renames, removals, and rotation reopening use the async filesystem path.
- The returned sink implements `Sink::write_async(...)`; the synchronous `write(...)` surface remains available for synchronous callers.
- On non-native targets, the sink follows the existing unavailable-backend behavior.
### How to Use
```moonbit
let sink = file_sink_async("app.log", auto_flush=false)
let logger = async_logger(sink)
```
### Error Case
- If the initial async open fails, the returned sink is unavailable and increments `open_failures()`.
- Later async writes record write and rotation failures through the existing sink counters.
### Notes
1. Use `file_sink(...)` for synchronous construction outside an async event loop.
2. Pair this API with `native_files_supported()` when code must also compile for non-native targets.
+2
View File
@@ -98,3 +98,5 @@ e.g.:
3. Non-native targets can still compile code referencing this API, but callers should treat actual file availability and successful writes as target-sensitive runtime behavior.
4. See [target-verification.md](./target-verification.md) for the current verification boundary between design intent and locally re-checked targets.
5. Use [`file_sink_async(...)`](./file-sink-async.md) when constructing a file sink from an async event loop.
+1
View File
@@ -153,6 +153,7 @@ BitLogger API navigation.
- [patch-sink.md](./patch-sink.md)
- [patch-sink-type.md](./patch-sink-type.md)
- [file-sink.md](./file-sink.md)
- [file-sink-async.md](./file-sink-async.md)
- [file-sink-available.md](./file-sink-available.md)
- [file-sink-flush.md](./file-sink-flush.md)
- [file-sink-close.md](./file-sink-close.md)
+18
View File
@@ -0,0 +1,18 @@
## BitLogger Update Changes
version 1.2.1(0.8.1)
### Changes
- Add deterministic QuickCheck properties for bounded history retention, queued overflow and drain behavior, patch composition, and logger config round-trips.
### Verification
- moon fmt --check
- moon check --deny-warn
- moon test --deny-warn across native, wasm, wasm-gc, and js targets
- pnpm run docs:build
### Notes
- QuickCheck seeds and shrink budgets are fixed for reproducible failures.
+1
View File
@@ -2,6 +2,7 @@
Versioned BitLogger change summaries.
- [1.2.1](./1.2.1(0.8.1).md)
- [1.2.0](./1.2.0(0.8.0).md)
- [1.1.3](./1.1.3(0.7.3).md)
- [1.1.2](./1.1.2(0.7.2).md)
+1 -1
View File
@@ -5,7 +5,7 @@ Use this flow for command-line tools and services that need a human-readable sta
## Install And Import
```bash
moon add Nanaloveyuki/BitLogger@0.8.0
moon add Nanaloveyuki/BitLogger@0.8.1
```
```moonbit
+1 -1
View File
@@ -7,7 +7,7 @@
```bash
moon new log-demo
cd log-demo
moon add Nanaloveyuki/BitLogger@0.8.0
moon add Nanaloveyuki/BitLogger@0.8.1
```
在应用的 `moon.pkg` 中加入:
+2 -2
View File
@@ -1,9 +1,9 @@
name = "Nanaloveyuki/BitLogger"
version = "0.8.0"
version = "0.8.1"
import {
"moonbitlang/async@0.20.2",
"moonbitlang/async@0.21.0",
}
readme = "src/README.mbt.md"
+28 -8
View File
@@ -132,6 +132,7 @@ pub struct AsyncLogger[S] {
flush_policy : AsyncFlushPolicy
sink : S
flush_callback : (S) -> Unit raise
flush_async_callback : (async (S) -> Unit)?
context_fields : Array[@bitlogger.Field]
filter : (@bitlogger.Record) -> Bool
patch : @bitlogger.RecordPatch
@@ -160,6 +161,7 @@ pub fn[S] async_logger(
flush_policy: config.flush,
sink,
flush_callback: flush,
flush_async_callback: None,
context_fields: [],
filter: fn(_) { true },
patch: @bitlogger.identity_patch(),
@@ -171,6 +173,14 @@ pub fn[S] async_logger(
}
}
///|
fn[S] with_async_flush_callback_internal(
logger : AsyncLogger[S],
callback : async (S) -> Unit,
) -> AsyncLogger[S] {
{ ..logger, flush_async_callback: Some(callback) }
}
///|
fn async_logger_phase_is_closed(phase : AsyncLifecyclePhase) -> Bool {
match phase {
@@ -535,8 +545,11 @@ pub async fn[S] AsyncLogger::shutdown(
}
///|
fn[S] run_flush_callback(logger : AsyncLogger[S]) -> Unit raise {
(logger.flush_callback)(logger.sink)
async fn[S] run_flush_callback(logger : AsyncLogger[S]) -> Unit {
match logger.flush_async_callback {
Some(callback) => callback(logger.sink)
None => (logger.flush_callback)(logger.sink)
}
}
///|
@@ -546,7 +559,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
err if err is AsyncLoggerClosed => break
err => raise err
}
logger.sink.write(rec)
logger.sink.write_async(rec)
if logger.pending_count.val > 0 {
logger.pending_count.val -= 1
}
@@ -557,7 +570,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
}
match next {
Some(next) => {
logger.sink.write(next)
logger.sink.write_async(next)
if logger.pending_count.val > 0 {
logger.pending_count.val -= 1
}
@@ -575,7 +588,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
}
match waited {
Some(next) => {
logger.sink.write(next)
logger.sink.write_async(next)
if logger.pending_count.val > 0 {
logger.pending_count.val -= 1
}
@@ -598,6 +611,7 @@ async fn[S : @bitlogger.Sink] run_worker(logger : AsyncLogger[S]) -> Unit {
}
///|
#warnings("-fragile_catch_all")
pub async fn[S : @bitlogger.Sink] AsyncLogger::run(
self : AsyncLogger[S],
) -> Unit {
@@ -619,14 +633,20 @@ pub async fn[S : @bitlogger.Sink] AsyncLogger::run(
pub fn build_async_logger(
config : AsyncLoggerBuildConfig,
) -> AsyncLogger[@bitlogger.RuntimeSink] {
let logger = @bitlogger.build_logger(config.logger)
async_logger(
let logger = @bitlogger.build_logger_for_async(config.logger)
let logger = async_logger(
logger.sink,
config=config.async_config,
min_level=logger.min_level,
target=logger.target,
flush=fn(sink) { ignore(sink.flush_progress()) },
).with_timestamp(enabled=logger.timestamp)
)
let flush_async : async (@bitlogger.RuntimeSink) -> Unit = sink => {
sink.flush_async()
}
with_async_flush_callback_internal(logger, flush_async).with_timestamp(
enabled=logger.timestamp,
)
}
///|
@@ -5,7 +5,7 @@ async test "native async file lifecycle flushes rotates and restarts" {
} else {
let path = "logs/bitlogger-async-native-e2e.log"
let first_flushes : Ref[Int] = Ref(0)
let first_sink = @bitlogger.file_sink(
let first_sink = @bitlogger.file_sink_async(
path,
append=false,
auto_flush=false,
@@ -47,7 +47,7 @@ async test "native async file lifecycle flushes rotates and restarts" {
inspect(first_sink.close(), content="true")
let restart_flushes : Ref[Int] = Ref(0)
let restarted_sink = @bitlogger.file_sink(
let restarted_sink = @bitlogger.file_sink_async(
path,
append=true,
auto_flush=false,
+1
View File
@@ -77,6 +77,7 @@ pub struct AsyncLogger[S] {
flush_policy : @utils.AsyncFlushPolicy
sink : S
flush_callback : (S) -> Unit raise
flush_async_callback : (async (S) -> Unit)?
context_fields : Array[@core.Field]
filter : (@core.Record) -> Bool
patch : (@core.Record) -> @core.Record
+228 -13
View File
@@ -45,6 +45,7 @@ pub struct FileSink {
priv default_auto_flush : Bool
priv rotation : Ref[FileRotation?]
priv default_rotation : FileRotation?
priv deferred : Bool
priv open_failures : Ref[Int]
priv write_failures : Ref[Int]
priv flush_failures : Ref[Int]
@@ -65,7 +66,64 @@ pub fn file_sink(
rotation? : FileRotation? = None,
formatter? : RecordFormatter = fn(rec) { @formatting.format_text(rec) },
) -> FileSink {
let handle = @utils.open_file_handle_internal(path, append)
new_file_sink_internal(
path,
append,
auto_flush,
rotation,
formatter,
@utils.open_file_handle_internal(path, append),
)
}
///|
pub async fn file_sink_async(
path : String,
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
formatter? : RecordFormatter = fn(rec) { @formatting.format_text(rec) },
) -> FileSink {
new_file_sink_internal(
path,
append,
auto_flush,
rotation,
formatter,
@utils.open_file_handle_async_internal(path, append),
)
}
///|
#doc(hidden)
pub fn file_sink_deferred(
path : String,
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
formatter? : RecordFormatter = fn(rec) { @formatting.format_text(rec) },
) -> FileSink {
new_file_sink_internal(
path,
append,
auto_flush,
rotation,
formatter,
@utils.deferred_file_handle_internal(path, append),
deferred=true,
)
}
///|
fn new_file_sink_internal(
path : String,
append : Bool,
auto_flush : Bool,
rotation : FileRotation?,
formatter : RecordFormatter,
handle : FileHandle?,
deferred? : Bool = false,
) -> FileSink {
{
path,
append: Ref(append),
@@ -76,6 +134,7 @@ pub fn file_sink(
default_auto_flush: auto_flush,
rotation: Ref(rotation),
default_rotation: rotation,
deferred,
open_failures: Ref(if handle is Some(_) { 0 } else { 1 }),
write_failures: Ref(0),
flush_failures: Ref(0),
@@ -103,6 +162,20 @@ pub fn FileSink::flush(self : FileSink) -> Bool {
}
}
///|
pub async fn FileSink::flush_async(self : FileSink) -> Bool {
match self.handle.val {
None => false
Some(handle) => {
let ok = @utils.flush_file_handle_async_internal(handle)
if !ok {
self.flush_failures.val += 1
}
ok
}
}
}
///|
pub fn FileSink::append_mode(self : FileSink) -> Bool {
self.append.val
@@ -274,20 +347,38 @@ pub fn FileSink::state(self : FileSink) -> FileSinkState {
pub fn FileSink::reopen(self : FileSink, append? : Bool? = None) -> Bool {
let append_mode = append.unwrap_or(self.append.val)
self.append.val = append_mode
match self.handle.val {
None => ()
Some(handle) => {
ignore(@utils.close_file_handle_internal(handle))
self.handle.val = None
if self.deferred {
match self.handle.val {
None => ()
Some(handle) => {
ignore(@utils.close_file_handle_internal(handle))
self.handle.val = None
}
}
let reopened = @utils.deferred_file_handle_internal(self.path, append_mode)
self.handle.val = reopened
if reopened is Some(_) {
true
} else {
self.open_failures.val += 1
false
}
}
let reopened = @utils.open_file_handle_internal(self.path, append_mode)
self.handle.val = reopened
if reopened is Some(_) {
true
} else {
self.open_failures.val += 1
false
match self.handle.val {
None => ()
Some(handle) => {
ignore(@utils.close_file_handle_internal(handle))
self.handle.val = None
}
}
let reopened = @utils.open_file_handle_internal(self.path, append_mode)
self.handle.val = reopened
if reopened is Some(_) {
true
} else {
self.open_failures.val += 1
false
}
}
}
@@ -333,6 +424,29 @@ fn rename_file_if_present(
}
}
///|
async fn remove_file_if_present_async(path : String) -> Bool {
if !@utils.file_exists_async_internal(path) {
true
} else {
@utils.remove_file_async_internal(path) ||
!@utils.file_exists_async_internal(path)
}
}
///|
async fn rename_file_if_present_async(
from_path : String,
to_path : String,
) -> Bool {
if !@utils.file_exists_async_internal(from_path) {
true
} else {
@utils.rename_file_async_internal(from_path, to_path) ||
!@utils.file_exists_async_internal(from_path)
}
}
///|
fn rotate_file_sink_internal(sink : FileSink, rotation : FileRotation) -> Bool {
let closed = match sink.handle.val {
@@ -396,6 +510,73 @@ fn rotate_if_needed_internal(sink : FileSink, next_line_bytes : Int) -> Bool {
}
}
///|
async fn rotate_file_sink_async_internal(
sink : FileSink,
rotation : FileRotation,
) -> Bool {
let closed = match sink.handle.val {
None => true
Some(handle) => {
let ok = @utils.close_file_handle_internal(handle)
sink.handle.val = None
ok
}
}
if !closed {
return false
}
if rotation.max_backups > 0 {
if !remove_file_if_present_async(
rotated_file_path(sink.path, rotation.max_backups),
) {
return false
}
for index = rotation.max_backups - 1; index >= 1; {
let from_path = rotated_file_path(sink.path, index)
let to_path = rotated_file_path(sink.path, index + 1)
if !rename_file_if_present_async(from_path, to_path) {
return false
}
continue index - 1
}
if !rename_file_if_present_async(sink.path, rotated_file_path(sink.path, 1)) {
return false
}
} else if !remove_file_if_present_async(sink.path) {
return false
}
sink.handle.val = @utils.open_file_handle_async_internal(sink.path, false)
sink.handle.val is Some(_)
}
///|
async fn rotate_if_needed_async_internal(
sink : FileSink,
next_line_bytes : Int,
) -> Bool {
match sink.rotation.val {
None => true
Some(rotation) =>
match sink.handle.val {
None => false
Some(handle) => {
let size = @utils.file_size_i64_async_internal(handle)
let next_line = next_line_bytes.to_int64()
if size + next_line <= rotation_max_bytes_internal(rotation) {
true
} else {
let rotated = rotate_file_sink_async_internal(sink, rotation)
if !rotated {
sink.rotation_failures.val += 1
}
rotated
}
}
}
}
}
///|
pub impl @sink_graph.Sink for FileSink with fn write(self, rec : Record) {
match self.handle.val {
@@ -429,3 +610,37 @@ pub impl @sink_graph.Sink for FileSink with fn write(self, rec : Record) {
}
}
}
///|
pub impl @sink_graph.Sink for FileSink with fn write_async(self, rec : Record) {
match self.handle.val {
None => self.write_failures.val += 1
Some(_) => {
let line = "\{(self.formatter)(rec)}\n"
let can_write = rotate_if_needed_async_internal(
self,
@utils.string_byte_length_internal(line),
)
if can_write {
match self.handle.val {
None => self.write_failures.val += 1
Some(active) => {
let wrote = @utils.write_file_handle_async_internal(active, line)
if wrote {
if self.auto_flush.val {
let flushed = @utils.flush_file_handle_async_internal(active)
if !flushed {
self.flush_failures.val += 1
}
}
} else {
self.write_failures.val += 1
}
}
}
} else {
self.write_failures.val += 1
}
}
}
}
+24 -6
View File
@@ -12,8 +12,14 @@ test "file sink counts an injected partial backup-chain failure" {
rec.message
})
if seed.is_available() {
seed.write(@core.Record::new(@core.Level::Info, "1234567890"))
seed.write(@core.Record::new(@core.Level::Info, "abcdefghij"))
@sink_graph.Sink::write(
seed,
@core.Record::new(@core.Level::Info, "1234567890"),
)
@sink_graph.Sink::write(
seed,
@core.Record::new(@core.Level::Info, "abcdefghij"),
)
inspect(seed.rotation_failures(), content="0")
inspect(seed.close(), content="true")
@@ -33,7 +39,10 @@ test "file sink counts an injected partial backup-chain failure" {
}
},
}
sink.write(@core.Record::new(@core.Level::Info, "klmnopqrst"))
@sink_graph.Sink::write(
sink,
@core.Record::new(@core.Level::Info, "klmnopqrst"),
)
inspect(sink.rotation_failures(), content="1")
inspect(sink.state().rotation_failures, content="1")
@@ -65,8 +74,14 @@ test "file sink counts an injected oldest-backup removal failure" {
rec.message
})
if seed.is_available() {
seed.write(@core.Record::new(@core.Level::Info, "1234567890"))
seed.write(@core.Record::new(@core.Level::Info, "abcdefghij"))
@sink_graph.Sink::write(
seed,
@core.Record::new(@core.Level::Info, "1234567890"),
)
@sink_graph.Sink::write(
seed,
@core.Record::new(@core.Level::Info, "abcdefghij"),
)
inspect(seed.close(), content="true")
let oldest = @utils.open_file_handle_internal(test_path + ".2", true)
@@ -95,7 +110,10 @@ test "file sink counts an injected oldest-backup removal failure" {
@utils.rename_file_internal(from_path, to_path)
},
}
sink.write(@core.Record::new(@core.Level::Info, "klmnopqrst"))
@sink_graph.Sink::write(
sink,
@core.Record::new(@core.Level::Info, "klmnopqrst"),
)
inspect(sink.rotation_failures(), content="1")
inspect(sink.write_failures(), content="1")
+3
View File
@@ -10,6 +10,8 @@ import {
// Values
pub fn file_sink(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> FileSink
pub async fn file_sink_async(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> FileSink
pub fn native_files_supported() -> Bool
// Errors
@@ -24,6 +26,7 @@ pub fn FileSink::clear_rotation(Self) -> Unit
pub fn FileSink::close(Self) -> Bool
pub fn FileSink::default_policy(Self) -> @file_model.FileSinkPolicy
pub fn FileSink::flush(Self) -> Bool
pub async fn FileSink::flush_async(Self) -> Bool
pub fn FileSink::flush_failures(Self) -> Int
pub fn FileSink::is_available(Self) -> Bool
pub fn FileSink::open_failures(Self) -> Int
+2 -2
View File
@@ -916,7 +916,7 @@ fn parse_inline_markup(
formatter : TextFormatter,
) -> Array[StyledSegment] {
let segments : Array[StyledSegment] = []
let buffer = StringBuilder::new()
let buffer = StringBuilder()
let stack : Array[StyleFrame] = [{ tag: "", style: default_inline_style() }]
let chars = input.to_array()
let current_style = fn() { stack[stack.length() - 1].style }
@@ -990,7 +990,7 @@ fn render_styled_text(
let enabled = use_ansi_color(formatter.color_mode)
let scoped = formatter.with_style_markup(mode)
let segments = parse_inline_markup(text, scoped)
let out = StringBuilder::new()
let out = StringBuilder()
for segment in segments {
out.write_string(ansi_wrap_with_style(segment.text, segment.style, enabled))
}
+4
View File
@@ -20,3 +20,7 @@ import {
import {
"Nanaloveyuki/BitLogger/src/utils",
} for "wbtest"
import {
"moonbitlang/core/quickcheck",
} for "test"
+2
View File
@@ -75,6 +75,8 @@ pub fn file_rotation_i64(Int64, max_backups? : Int) -> @file_model.FileRotation
pub fn file_sink(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> @file_runtime.FileSink
pub async fn file_sink_async(String, append? : Bool, auto_flush? : Bool, rotation? : @file_model.FileRotation?, formatter? : (@core.Record) -> String) -> @file_runtime.FileSink
pub fn file_sink_policy_to_json(@file_model.FileSinkPolicy) -> Json
pub fn file_sink_state_to_json(@file_model.FileSinkState) -> Json
+400
View File
@@ -0,0 +1,400 @@
///|
fn pbt_level(seed : Int) -> Level {
match (seed & 0x3fffffff) % 5 {
0 => Level::Trace
1 => Level::Debug
2 => Level::Info
3 => Level::Warn
_ => Level::Error
}
}
///|
fn pbt_nonnegative(seed : Int, modulus : Int) -> Int {
(seed & 0x3fffffff) % modulus
}
///|
fn pbt_bit(seed : Int, bit : Int) -> Bool {
(seed & bit) != 0
}
///|
fn pbt_fields_equal(left : Array[Field], right : Array[Field]) -> Bool {
guard left.length() == right.length() else { return false }
for i in 0..<left.length() {
guard left[i].key == right[i].key && left[i].value == right[i].value else {
return false
}
}
true
}
///|
fn pbt_records_equal(left : Record, right : Record) -> Bool {
left.level.label() == right.level.label() &&
left.timestamp_ms == right.timestamp_ms &&
left.target == right.target &&
left.message == right.message &&
pbt_fields_equal(left.fields, right.fields)
}
///|
fn pbt_history_matches(
sink : HistorySink,
expected : Array[(String, String)],
expected_dropped : Int,
) -> Bool {
guard sink.count() == expected.length() else { return false }
guard sink.dropped_count() == expected_dropped else { return false }
let snapshot = sink.snapshot()
guard snapshot.length() == expected.length() else { return false }
for i in 0..<expected.length() {
let (message, id) = expected[i]
let record = snapshot[i]
guard record.message == message else { return false }
guard record.fields.length() == 1 else { return false }
guard record.fields[0].key == "id" else { return false }
guard record.fields[0].value == id else { return false }
}
// The public snapshot must not expose the sink's retained field arrays.
if snapshot.length() > 0 {
snapshot[0].fields.push(field("mutated", "outside"))
guard sink.snapshot()[0].fields.length() == 1 else { return false }
}
true
}
///|
test "quickcheck: history sink agrees with a bounded model" {
@quickcheck.check(
(input : (Int, Array[(Int, Int)])) => {
let (capacity_seed, operations) = input
let requested_capacity = capacity_seed % 8
let capacity = if requested_capacity <= 0 {
1
} else {
requested_capacity
}
let sink = history_sink(capacity=requested_capacity)
let mut expected : Array[(String, String)] = []
let mut expected_dropped = 0
for operation in operations {
let (kind, value) = operation
if (kind & 7) == 0 {
sink.clear()
expected = []
expected_dropped = 0
} else {
let id = value.to_string()
let message = "message:" + id
let record = Record::new(Level::Info, message, fields=[
field("id", id),
])
@sink_graph.Sink::write(sink, record)
if expected.length() >= capacity {
ignore(expected.remove(0))
expected_dropped += 1
}
expected.push((message, id))
}
guard pbt_history_matches(sink, expected, expected_dropped) else {
return false
}
}
pbt_history_matches(sink, expected, expected_dropped)
},
count=220,
max_size=80,
max_shrinks=200,
seed=0x51A7UL,
)
}
///|
fn pbt_queue_limit(seed : Int) -> Int {
match seed & 3 {
0 => -1
1 => 0
2 => 1
_ => 3
}
}
///|
fn pbt_drain_model(
expected : Array[String],
delivered : Array[String],
max_items : Int,
) -> Int {
if max_items == 0 {
return 0
}
let limit = if max_items < 0 { expected.length() } else { max_items }
let count = if limit > expected.length() { expected.length() } else { limit }
for _ in 0..<count {
delivered.push(expected.remove(0))
}
count
}
///|
fn pbt_queue_matches(
sink : QueuedSink[CallbackSink],
expected : Array[String],
expected_dropped : Int,
delivered : Ref[Array[String]],
expected_delivered : Array[String],
) -> Bool {
sink.pending_count() == expected.length() &&
sink.dropped_count() == expected_dropped &&
delivered.val == expected_delivered
}
///|
test "quickcheck: queued sink agrees with an overflow and drain model" {
@quickcheck.check(
(input : (Int, Int, Array[(Int, Int)])) => {
let (max_pending_seed, policy_seed, operations) = input
let max_pending = pbt_nonnegative(max_pending_seed, 8)
let policy = if pbt_bit(policy_seed, 1) {
QueueOverflowPolicy::DropOldest
} else {
QueueOverflowPolicy::DropNewest
}
let delivered : Ref[Array[String]] = Ref([])
let sink = queued_sink(
callback_sink(fn(record) { delivered.val.push(record.message) }),
max_pending~,
overflow=policy,
)
let expected : Array[String] = []
let expected_delivered : Array[String] = []
let mut expected_dropped = 0
for operation in operations {
let (kind, value) = operation
match kind & 7 {
0 | 1 | 2 | 3 => {
let message = "message:" + value.to_string()
let record = Record::new(Level::Info, message)
let full = max_pending > 0 && expected.length() >= max_pending
if full {
expected_dropped += 1
match policy {
QueueOverflowPolicy::DropNewest => ()
QueueOverflowPolicy::DropOldest => {
ignore(expected.remove(0))
expected.push(message)
}
}
} else {
expected.push(message)
}
@sink_graph.Sink::write(sink, record)
}
4 => {
let limit = pbt_queue_limit(value)
let expected_count = pbt_drain_model(
expected, expected_delivered, limit,
)
let actual_count = sink.drain(max_items=limit)
guard actual_count == expected_count else { return false }
}
5 => {
let expected_count = pbt_drain_model(
expected, expected_delivered, -1,
)
guard sink.flush() == expected_count else { return false }
}
_ => {
let expected_count = pbt_drain_model(
expected, expected_delivered, 0,
)
guard expected_count == 0 else { return false }
guard sink.drain(max_items=0) == 0 else { return false }
}
}
guard pbt_queue_matches(
sink, expected, expected_dropped, delivered, expected_delivered,
) else {
return false
}
}
pbt_queue_matches(
sink, expected, expected_dropped, delivered, expected_delivered,
)
},
count=220,
max_size=80,
max_shrinks=200,
seed=0xA11CEUL,
)
}
///|
fn pbt_patch_for(operation : (Int, String)) -> RecordPatch {
let (kind, value) = operation
match kind & 7 {
0 => identity_patch()
1 => set_target("target." + value)
2 => prefix_message("[" + value + "] ")
3 => append_fields([field("extra", value)])
4 => redact_field("secret", placeholder=value)
_ => redact_fields(["secret", "token"], placeholder=value)
}
}
///|
test "quickcheck: composed patches match sequential application" {
@quickcheck.check(
(input : (Array[(Int, String)], (Int, String, Array[(String, String)]))) => {
let (operations, record_input) = input
let (level_seed, message, raw_fields) = record_input
let base = Record::new(
pbt_level(level_seed),
message,
timestamp_ms=123UL,
target="service",
fields=raw_fields.map(entry => field(entry.0, entry.1)),
)
let patches : Array[RecordPatch] = operations.map(pbt_patch_for)
let mut sequential = base
for patch in patches {
sequential = patch(sequential)
}
let composed = compose_patches(patches)(base)
pbt_records_equal(sequential, composed)
},
count=220,
max_size=70,
max_shrinks=200,
seed=0xC0DEUL,
)
}
///|
fn pbt_color_mode(seed : Int) -> ColorMode {
match pbt_nonnegative(seed, 3) {
0 => ColorMode::Never
1 => ColorMode::Auto
_ => ColorMode::Always
}
}
///|
fn pbt_color_support(seed : Int) -> ColorSupport {
if pbt_bit(seed, 1) {
ColorSupport::Basic
} else {
ColorSupport::TrueColor
}
}
///|
fn pbt_markup_mode(seed : Int) -> StyleMarkupMode {
match pbt_nonnegative(seed, 3) {
0 => StyleMarkupMode::Disabled
1 => StyleMarkupMode::Builtin
_ => StyleMarkupMode::Full
}
}
///|
fn pbt_sink_kind(seed : Int) -> SinkKind {
match pbt_nonnegative(seed, 4) {
0 => SinkKind::Console
1 => SinkKind::JsonConsole
2 => SinkKind::TextConsole
_ => SinkKind::File
}
}
///|
test "quickcheck: serialized logger configs parse to the same canonical text" {
@quickcheck.check(
(input : (Int, String, Bool, (Int, Int, Int, String))) => {
let (seed, target, timestamp, sink_input) = input
let (sink_seed, queue_seed, flags, suffix) = sink_input
let kind = pbt_sink_kind(sink_seed)
let path = match kind {
SinkKind::File =>
if suffix == "" {
"logs/generated.log"
} else {
"logs/" + suffix
}
_ => suffix
}
let style_tags : Map[String, TextStyle] = if pbt_bit(flags, 16) {
{ "accent": text_style(fg=Some(suffix), bold=pbt_bit(flags, 32)) }
} else {
Map([])
}
let formatter = TextFormatterConfig::new(
show_timestamp=pbt_bit(flags, 1),
show_level=pbt_bit(flags, 2),
show_target=pbt_bit(flags, 4),
show_fields=pbt_bit(flags, 8),
separator=if suffix == "" { " " } else { suffix },
field_separator=if target == "" { "," } else { target },
template="[{level}] {target} {message}",
color_mode=pbt_color_mode(seed),
color_support=pbt_color_support(sink_seed),
style_markup=pbt_markup_mode(flags),
target_style_markup=pbt_markup_mode(seed),
fields_style_markup=pbt_markup_mode(queue_seed),
style_tags~,
)
let rotation = if pbt_bit(sink_seed, 1) {
Some(
file_rotation(
1 + pbt_nonnegative(queue_seed, 64),
max_backups=1 + pbt_nonnegative(flags, 4),
),
)
} else {
None
}
let sink = SinkConfig::new(
kind~,
path~,
append=pbt_bit(flags, 1),
auto_flush=pbt_bit(flags, 2),
rotation~,
text_formatter=formatter,
)
let queue = if pbt_bit(queue_seed, 1) {
Some(
QueueConfig::new(
queue_seed % 17,
overflow=if pbt_bit(queue_seed, 2) {
QueueOverflowPolicy::DropOldest
} else {
QueueOverflowPolicy::DropNewest
},
),
)
} else {
None
}
let config = LoggerConfig::new(
min_level=pbt_level(seed),
target~,
timestamp~,
sink~,
queue~,
)
let serialized = stringify_logger_config(config)
(fn() -> Bool raise ConfigError {
let parsed = parse_logger_config_text(serialized)
stringify_logger_config(parsed) == serialized
})() catch {
_ => false
}
},
count=180,
max_size=60,
max_shrinks=200,
seed=0xBEEF1234UL,
)
}
+1
View File
@@ -68,6 +68,7 @@ pub fn RuntimeSink::file_state(Self) -> @file_model.FileSinkState
pub fn RuntimeSink::file_state_or_none(Self) -> @file_model.FileSinkState?
pub fn RuntimeSink::file_write_failures(Self) -> Int
pub fn RuntimeSink::flush(Self) -> Int
pub async fn RuntimeSink::flush_async(Self) -> Unit
pub fn RuntimeSink::flush_progress(Self) -> RuntimeSinkProgress
pub fn RuntimeSink::pending_count(Self) -> Int
pub impl @sink_graph.Sink for RuntimeSink
+73 -13
View File
@@ -81,6 +81,20 @@ pub impl @sink_graph.Sink for RuntimeSink with fn write(self, rec) {
}
}
///|
pub impl @sink_graph.Sink for RuntimeSink with fn write_async(self, rec) {
match self {
Console(sink) => sink.write(rec)
JsonConsole(sink) => sink.write(rec)
TextConsole(sink) => sink.write(rec)
File(sink) => sink.write_async(rec)
QueuedConsole(sink) => sink.write(rec)
QueuedJsonConsole(sink) => sink.write(rec)
QueuedTextConsole(sink) => sink.write(rec)
QueuedFile(sink) => sink.write(rec)
}
}
///|
fn[S : @sink_graph.Sink] queued_sink_close_internal(
sink : QueuedSink[S],
@@ -202,6 +216,23 @@ pub fn RuntimeSink::flush(self : RuntimeSink) -> Int {
runtime_sink_progress_compat_count(self.flush_progress())
}
///|
pub async fn RuntimeSink::flush_async(self : RuntimeSink) -> Unit {
match self {
Console(_) => ()
JsonConsole(_) => ()
TextConsole(_) => ()
File(sink) => ignore(sink.flush_async())
QueuedConsole(sink) => ignore(sink.flush_async())
QueuedJsonConsole(sink) => ignore(sink.flush_async())
QueuedTextConsole(sink) => ignore(sink.flush_async())
QueuedFile(sink) => {
ignore(sink.flush_async())
ignore(sink.sink.flush_async())
}
}
}
///|
pub fn RuntimeSink::drain_progress(
self : RuntimeSink,
@@ -671,7 +702,10 @@ pub fn RuntimeSink::file_runtime_state(self : RuntimeSink) -> RuntimeFileState?
}
///|
pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
fn build_runtime_sink_internal(
config : SinkConfig,
defer_file_open : Bool,
) -> RuntimeSink {
match config.kind {
SinkKind::Console => RuntimeSink::Console(@sink_graph.console_sink())
SinkKind::JsonConsole =>
@@ -682,22 +716,48 @@ pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
)
SinkKind::File =>
RuntimeSink::File(
@file_runtime.file_sink(
config.path,
append=config.append,
auto_flush=config.auto_flush,
rotation=config.rotation,
formatter=fn(rec) {
@formatting.format_text(
rec,
formatter=config.text_formatter.to_formatter(),
)
},
),
if defer_file_open {
@file_runtime.file_sink_deferred(
config.path,
append=config.append,
auto_flush=config.auto_flush,
rotation=config.rotation,
formatter=fn(rec) {
@formatting.format_text(
rec,
formatter=config.text_formatter.to_formatter(),
)
},
)
} else {
@file_runtime.file_sink(
config.path,
append=config.append,
auto_flush=config.auto_flush,
rotation=config.rotation,
formatter=fn(rec) {
@formatting.format_text(
rec,
formatter=config.text_formatter.to_formatter(),
)
},
)
},
)
}
}
///|
pub fn build_runtime_sink(config : SinkConfig) -> RuntimeSink {
build_runtime_sink_internal(config, false)
}
///|
#doc(hidden)
pub fn build_runtime_sink_for_async(config : SinkConfig) -> RuntimeSink {
build_runtime_sink_internal(config, true)
}
///|
pub fn apply_queue_config(
sink : RuntimeSink,
+20 -2
View File
@@ -64,8 +64,15 @@ pub fn Logger::dropped_count(self : Logger[RuntimeSink]) -> Int {
}
///|
pub fn build_logger(config : LoggerConfig) -> Logger[RuntimeSink] {
let sink = @runtime.build_runtime_sink(config.sink)
fn build_logger_internal(
config : LoggerConfig,
defer_file_open : Bool,
) -> Logger[RuntimeSink] {
let sink = if defer_file_open {
@runtime.build_runtime_sink_for_async(config.sink)
} else {
@runtime.build_runtime_sink(config.sink)
}
let actual_sink = match config.queue {
None => sink
Some(queue) => @runtime.apply_queue_config(sink, queue)
@@ -75,6 +82,17 @@ pub fn build_logger(config : LoggerConfig) -> Logger[RuntimeSink] {
)
}
///|
pub fn build_logger(config : LoggerConfig) -> Logger[RuntimeSink] {
build_logger_internal(config, false)
}
///|
#doc(hidden)
pub fn build_logger_for_async(config : LoggerConfig) -> Logger[RuntimeSink] {
build_logger_internal(config, true)
}
///|
pub fn parse_and_build_logger(
input : String,
+4
View File
@@ -51,6 +51,7 @@ pub struct BufferedSink[S] {
flush_limit : Int
}
pub fn[S : Sink] BufferedSink::flush(Self[S]) -> Unit
pub async fn[S : Sink] BufferedSink::flush_async(Self[S]) -> Unit
pub fn[S] BufferedSink::pending_count(Self[S]) -> Int
pub impl[S : Sink] Sink for BufferedSink[S]
@@ -124,8 +125,10 @@ pub struct QueuedSink[S] {
dropped_count : @ref.Ref[Int]
}
pub fn[S : Sink] QueuedSink::drain(Self[S], max_items? : Int) -> Int
pub async fn[S : Sink] QueuedSink::drain_async(Self[S], max_items? : Int) -> Int
pub fn[S] QueuedSink::dropped_count(Self[S]) -> Int
pub fn[S : Sink] QueuedSink::flush(Self[S]) -> Int
pub async fn[S : Sink] QueuedSink::flush_async(Self[S]) -> Int
pub fn[S] QueuedSink::pending_count(Self[S]) -> Int
pub impl[S] Sink for QueuedSink[S]
@@ -142,4 +145,5 @@ pub using @queue_model {type QueueOverflowPolicy}
// Traits
pub(open) trait Sink {
fn write(Self, @core.Record) -> Unit
async fn write_async(Self, @core.Record) -> Unit = _
}
+101
View File
@@ -19,6 +19,12 @@ type RecordPatch = @record_ops.RecordPatch
///|
pub(open) trait Sink {
fn write(Self, Record) -> Unit
async fn write_async(Self, Record) -> Unit = _
}
///|
impl Sink with fn write_async(self : Self, rec : Record) {
self.write(rec)
}
///|
@@ -63,6 +69,18 @@ pub impl[S : Sink] Sink for ContextSink[S] with fn write(self, rec) {
self.sink.write(rec.with_fields(merged))
}
///|
pub impl[S : Sink] Sink for ContextSink[S] with fn write_async(self, rec) {
let merged = if self.context_fields.length() == 0 {
rec.fields
} else if rec.fields.length() == 0 {
self.context_fields
} else {
self.context_fields + rec.fields
}
self.sink.write_async(rec.with_fields(merged))
}
///|
pub struct JsonConsoleSink {
_dummy : Unit
@@ -148,6 +166,15 @@ pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with fn write(self, rec)
self.right.write(rec.copy())
}
///|
pub impl[A : Sink, B : Sink] Sink for FanoutSink[A, B] with fn write_async(
self,
rec,
) {
self.left.write_async(rec)
self.right.write_async(rec.copy())
}
///|
pub struct SplitSink[A, B] {
left : A
@@ -182,6 +209,18 @@ pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with fn write(self, rec) {
}
}
///|
pub impl[A : Sink, B : Sink] Sink for SplitSink[A, B] with fn write_async(
self,
rec,
) {
if (self.predicate)(rec) {
self.left.write_async(rec)
} else {
self.right.write_async(rec)
}
}
///|
pub struct CallbackSink {
callback : (Record) -> Unit
@@ -299,6 +338,29 @@ pub impl[S : Sink] Sink for BufferedSink[S] with fn write(self, rec) {
}
}
///|
pub async fn[S : Sink] BufferedSink::flush_async(
self : BufferedSink[S],
) -> Unit {
if self.buffer.val.length() == 0 {
()
} else {
let pending = self.buffer.val
self.buffer.val = []
for rec in pending {
self.sink.write_async(rec)
}
}
}
///|
pub impl[S : Sink] Sink for BufferedSink[S] with fn write_async(self, rec) {
self.buffer.val.push(rec)
if self.buffer.val.length() >= self.flush_limit {
self.flush_async()
}
}
///|
pub type QueueOverflowPolicy = @queue_model.QueueOverflowPolicy
@@ -358,11 +420,38 @@ pub fn[S : Sink] QueuedSink::drain(
}
}
///|
pub async fn[S : Sink] QueuedSink::drain_async(
self : QueuedSink[S],
max_items? : Int = -1,
) -> Int {
if max_items == 0 {
return 0
}
let limit = if max_items < 0 { self.pending_count() } else { max_items }
for drained = 0; drained < limit; {
match self.queue.pop() {
None => break drained
Some(rec) => {
self.sink.write_async(rec)
continue drained + 1
}
}
} nobreak {
limit
}
}
///|
pub fn[S : Sink] QueuedSink::flush(self : QueuedSink[S]) -> Int {
self.drain()
}
///|
pub async fn[S : Sink] QueuedSink::flush_async(self : QueuedSink[S]) -> Int {
self.drain_async()
}
///|
pub impl[S] Sink for QueuedSink[S] with fn write(self, rec) {
let full = self.max_pending > 0 && self.pending_count() >= self.max_pending
@@ -398,6 +487,13 @@ pub impl[S : Sink] Sink for FilterSink[S] with fn write(self, rec) {
}
}
///|
pub impl[S : Sink] Sink for FilterSink[S] with fn write_async(self, rec) {
if (self.predicate)(rec) {
self.sink.write_async(rec)
}
}
///|
pub struct PatchSink[S] {
sink : S
@@ -413,3 +509,8 @@ pub fn[S] patch_sink(sink : S, patch : RecordPatch) -> PatchSink[S] {
pub impl[S : Sink] Sink for PatchSink[S] with fn write(self, rec) {
self.sink.write((self.patch)(rec))
}
///|
pub impl[S : Sink] Sink for PatchSink[S] with fn write_async(self, rec) {
self.sink.write_async((self.patch)(rec))
}
+17
View File
@@ -64,3 +64,20 @@ pub fn file_sink(
) -> FileSink {
@file_runtime.file_sink(path, append~, auto_flush~, rotation~, formatter~)
}
///|
pub async fn file_sink_async(
path : String,
append? : Bool = true,
auto_flush? : Bool = true,
rotation? : FileRotation? = None,
formatter? : RecordFormatter = fn(rec) { format_text(rec) },
) -> FileSink {
@file_runtime.file_sink_async(
path,
append~,
auto_flush~,
rotation~,
formatter~,
)
}
+183 -95
View File
@@ -1,106 +1,137 @@
///|
fn string_to_c_bytes(str : String) -> Bytes {
let res : Array[Byte] = []
let len = str.length()
let mut i = 0
while i < len {
let mut c = str.code_unit_at(i).to_int()
if 0xD800 <= c && c <= 0xDBFF {
c -= 0xD800
i = i + 1
let l = str.code_unit_at(i).to_int() - 0xDC00
c = (c << 10) + l + 0x10000
}
if c < 0x80 {
res.push(c.to_byte())
} else if c < 0x800 {
res.push((0xc0 + (c >> 6)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
} else if c < 0x10000 {
res.push((0xe0 + (c >> 12)).to_byte())
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
} else {
res.push((0xf0 + (c >> 18)).to_byte())
res.push((0x80 + ((c >> 12) & 0x3f)).to_byte())
res.push((0x80 + ((c >> 6) & 0x3f)).to_byte())
res.push((0x80 + (c & 0x3f)).to_byte())
}
i = i + 1
}
res.push((0).to_byte())
Bytes::from_array(res)
pub struct FileHandle {
priv path : String
priv append : Bool
priv initialized : Ref[Bool]
priv position : Ref[Int64]
priv closed : Ref[Bool]
}
///|
#external
type NativeFileHandle
fn run_async_bool_internal(operation : async () -> Bool) -> Bool {
let result = Ref(false)
@async.run_async_main(() => result.val = operation())
result.val
}
///|
#borrow(path, mode)
extern "C" fn file_open_ffi(path : Bytes, mode : Bytes) -> NativeFileHandle = "bitlogger_file_open"
fn run_async_i64_internal(operation : async () -> Int64) -> Int64 {
let result = Ref(0L)
@async.run_async_main(() => result.val = operation())
result.val
}
///|
extern "C" fn file_is_null_ffi(handle : NativeFileHandle) -> Bool = "bitlogger_pointer_is_null"
fn new_file_handle_internal(
path : String,
append : Bool,
initialized : Bool,
) -> FileHandle {
{
path,
append,
initialized: Ref(initialized),
position: Ref(0L),
closed: Ref(false),
}
}
///|
#borrow(buffer)
extern "C" fn file_write_ffi(
buffer : Bytes,
size : Int,
count : Int,
handle : NativeFileHandle,
) -> Int = "bitlogger_file_write"
async fn probe_open_file_handle_async_internal(
path : String,
append : Bool,
) -> Bool {
try {
let create_mode = if append {
@fs.OpenOrCreate
} else {
@fs.CreateOrTruncate
}
let file = @fs.open(path, mode=@fs.WriteOnly, append~, create_mode~)
file.close()
true
} catch {
_ => false
}
}
///|
extern "C" fn file_flush_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_flush"
///|
extern "C" fn file_close_ffi(handle : NativeFileHandle) -> Int = "bitlogger_file_close"
///|
extern "C" fn file_seek_ffi(
handle : NativeFileHandle,
offset : Int,
origin : Int,
) -> Int = "bitlogger_file_seek"
///|
extern "C" fn file_tell_i64_ffi(handle : NativeFileHandle) -> Int64 = "bitlogger_file_tell_i64"
///|
#borrow(from_path, to_path)
extern "C" fn file_rename_ffi(from_path : Bytes, to_path : Bytes) -> Int = "bitlogger_file_rename"
///|
#borrow(path)
extern "C" fn file_remove_ffi(path : Bytes) -> Int = "bitlogger_file_remove"
///|
pub struct FileHandle {
path : String
raw : NativeFileHandle
pub async fn open_file_handle_async_internal(
path : String,
append : Bool,
) -> FileHandle? {
if probe_open_file_handle_async_internal(path, append) {
Some(new_file_handle_internal(path, append, true))
} else {
None
}
}
///|
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
let mode = if append { "ab" } else { "wb" }
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes(mode))
if file_is_null_ffi(raw) {
None
let opened = run_async_bool_internal(() => {
probe_open_file_handle_async_internal(path, append)
})
if opened {
Some(new_file_handle_internal(path, append, true))
} else {
Some({ raw, path })
None
}
}
///|
pub fn deferred_file_handle_internal(
path : String,
append : Bool,
) -> FileHandle? {
Some(new_file_handle_internal(path, append, false))
}
///|
pub async fn write_file_handle_async_internal(
handle : FileHandle,
content : String,
) -> Bool {
if handle.closed.val {
return false
}
let bytes = @utf8.encode(content)
try {
let file = @fs.open(
handle.path,
mode=@fs.WriteOnly,
append=handle.append,
create_mode=if handle.initialized.val {
@fs.OpenOrCreate
} else if handle.append {
@fs.OpenOrCreate
} else {
@fs.CreateOrTruncate
},
)
defer file.close()
if handle.append {
file.write(bytes)
} else {
file.write_at(bytes[:], position=handle.position.val)
handle.position.val += bytes.length().to_int64()
}
handle.initialized.val = true
true
} catch {
_ => false
}
}
///|
pub fn file_exists_internal(path : String) -> Bool {
let raw = file_open_ffi(string_to_c_bytes(path), string_to_c_bytes("rb"))
if file_is_null_ffi(raw) {
false
} else {
ignore(file_close_ffi(raw))
true
run_async_bool_internal(() => @fs.exists(path))
}
///|
pub async fn file_exists_async_internal(path : String) -> Bool {
@fs.exists(path) catch {
_ => false
}
}
@@ -109,45 +140,102 @@ pub fn write_file_handle_internal(
handle : FileHandle,
content : String,
) -> Bool {
let bytes = string_to_c_bytes(content)
let written = file_write_ffi(bytes, 1, bytes.length() - 1, handle.raw)
written == bytes.length() - 1
run_async_bool_internal(() => {
write_file_handle_async_internal(handle, content)
})
}
///|
pub async fn flush_file_handle_async_internal(handle : FileHandle) -> Bool {
if handle.closed.val {
return false
}
try {
let file = @fs.open(handle.path, mode=@fs.WriteOnly)
file.sync()
file.close()
true
} catch {
_ => false
}
}
///|
pub fn flush_file_handle_internal(handle : FileHandle) -> Bool {
file_flush_ffi(handle.raw) == 0
if handle.closed.val {
false
} else {
// Each write owns and closes its File, so there is no buffered handle to flush here.
true
}
}
///|
pub fn close_file_handle_internal(handle : FileHandle) -> Bool {
file_close_ffi(handle.raw) == 0
if handle.closed.val {
false
} else {
handle.closed.val = true
true
}
}
///|
pub async fn file_size_i64_async_internal(handle : FileHandle) -> Int64 {
if handle.closed.val {
return 0L
}
try {
let file = @fs.open(handle.path, mode=@fs.ReadOnly)
let size = file.size()
file.close()
size
} catch {
_ => 0L
}
}
///|
pub fn file_size_i64_internal(handle : FileHandle) -> Int64 {
ignore(file_seek_ffi(handle.raw, 0, 2))
let size = file_tell_i64_ffi(handle.raw)
if size < 0L {
0L
} else {
size
run_async_i64_internal(() => file_size_i64_async_internal(handle))
}
///|
pub async fn rename_file_async_internal(
from_path : String,
to_path : String,
) -> Bool {
try {
@fs.rename(from_path, to_path)
true
} catch {
_ => false
}
}
///|
pub fn rename_file_internal(from_path : String, to_path : String) -> Bool {
file_rename_ffi(string_to_c_bytes(from_path), string_to_c_bytes(to_path)) == 0
run_async_bool_internal(() => rename_file_async_internal(from_path, to_path))
}
///|
pub async fn remove_file_async_internal(path : String) -> Bool {
try {
@fs.remove(path)
true
} catch {
_ => false
}
}
///|
pub fn remove_file_internal(path : String) -> Bool {
file_remove_ffi(string_to_c_bytes(path)) == 0
run_async_bool_internal(() => remove_file_async_internal(path))
}
///|
pub fn string_byte_length_internal(content : String) -> Int {
string_to_c_bytes(content).length() - 1
@utf8.encode(content).length()
}
///|
+80 -2
View File
@@ -1,28 +1,106 @@
///|
pub struct FileHandle {
path : String
priv dummy : Unit
}
///|
pub fn open_file_handle_internal(path : String, append : Bool) -> FileHandle? {
ignore(append)
ignore(path)
let _unused : FileHandle = { path: "" }
let _unused : FileHandle = { dummy: () }
ignore(_unused)
None
}
///|
pub fn deferred_file_handle_internal(
path : String,
append : Bool,
) -> FileHandle? {
ignore(append)
ignore(path)
None
}
///|
pub fn file_exists_internal(path : String) -> Bool {
ignore(path)
false
}
///|
async fn stub_async_noop_internal() -> Unit {
ignore(@fs.unimplemented)
@async.pause()
}
///|
pub async fn open_file_handle_async_internal(
path : String,
append : Bool,
) -> FileHandle? {
stub_async_noop_internal()
ignore(append)
ignore(path)
None
}
///|
pub async fn file_exists_async_internal(path : String) -> Bool {
stub_async_noop_internal()
ignore(path)
false
}
///|
pub async fn write_file_handle_async_internal(
handle : FileHandle,
content : String,
) -> Bool {
stub_async_noop_internal()
ignore(handle)
ignore(content)
false
}
///|
pub async fn flush_file_handle_async_internal(handle : FileHandle) -> Bool {
stub_async_noop_internal()
ignore(handle)
false
}
///|
pub async fn file_size_i64_async_internal(handle : FileHandle) -> Int64 {
stub_async_noop_internal()
ignore(handle)
0L
}
///|
pub async fn rename_file_async_internal(
from_path : String,
to_path : String,
) -> Bool {
stub_async_noop_internal()
ignore(from_path)
ignore(to_path)
false
}
///|
pub async fn remove_file_async_internal(path : String) -> Bool {
stub_async_noop_internal()
ignore(path)
false
}
///|
pub fn write_file_handle_internal(
handle : FileHandle,
content : String,
) -> Bool {
ignore(handle.dummy)
ignore(handle)
ignore(content)
false
+3 -1
View File
@@ -2,14 +2,16 @@ import {
"Nanaloveyuki/BitLogger/src/core",
"Nanaloveyuki/BitLogger/src/formatting",
"Nanaloveyuki/BitLogger/src/record_ops",
"moonbitlang/async",
"moonbitlang/async/fs",
"moonbitlang/core/env",
"moonbitlang/core/encoding/utf8",
"moonbitlang/core/json",
"moonbitlang/core/ref",
"moonbitlang/core/string",
}
options(
"native-stub": [ "stub.c" ],
targets: {
"file_backend_native.mbt": [ "native", "llvm" ],
"file_backend_stub.mbt": [ "js", "wasm", "wasm-gc" ],
+17 -1
View File
@@ -23,12 +23,20 @@ pub fn compose_patches(Array[(@core.Record) -> @core.Record]) -> (@core.Record)
pub fn default_style_tag_registry() -> @formatting.StyleTagRegistry
pub fn deferred_file_handle_internal(String, Bool) -> FileHandle?
pub fn field_equals(String, String) -> (@core.Record) -> Bool
pub async fn file_exists_async_internal(String) -> Bool
pub fn file_exists_internal(String) -> Bool
pub async fn file_size_i64_async_internal(FileHandle) -> Int64
pub fn file_size_i64_internal(FileHandle) -> Int64
pub async fn flush_file_handle_async_internal(FileHandle) -> Bool
pub fn flush_file_handle_internal(FileHandle) -> Bool
pub fn format_json(@core.Record) -> String
@@ -49,6 +57,8 @@ pub fn native_files_supported_internal() -> Bool
pub fn not_((@core.Record) -> Bool) -> (@core.Record) -> Bool
pub async fn open_file_handle_async_internal(String, Bool) -> FileHandle?
pub fn open_file_handle_internal(String, Bool) -> FileHandle?
pub fn prefix_message(String) -> (@core.Record) -> @core.Record
@@ -57,8 +67,12 @@ pub fn redact_field(String, placeholder? : String) -> (@core.Record) -> @core.Re
pub fn redact_fields(Array[String], placeholder? : String) -> (@core.Record) -> @core.Record
pub async fn remove_file_async_internal(String) -> Bool
pub fn remove_file_internal(String) -> Bool
pub async fn rename_file_async_internal(String, String) -> Bool
pub fn rename_file_internal(String, String) -> Bool
pub fn reset_global_style_tag_registry() -> Unit
@@ -81,13 +95,15 @@ pub fn text_formatter(show_timestamp? : Bool, show_level? : Bool, show_target? :
pub fn text_style(fg? : String?, bg? : String?, bold? : Bool, dim? : Bool, italic? : Bool, underline? : Bool) -> @formatting.TextStyle
pub async fn write_file_handle_async_internal(FileHandle, String) -> Bool
pub fn write_file_handle_internal(FileHandle, String) -> Bool
// Errors
// Types and methods
pub struct FileHandle {
path : String
// private fields
}
// Type aliases
-55
View File
@@ -1,55 +0,0 @@
#include <stdint.h>
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
int32_t bitlogger_pointer_is_null(void *ptr) {
return ptr == 0;
}
void *bitlogger_file_open(const char *path, const char *mode) {
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
return fopen(path, mode);
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
}
int32_t bitlogger_file_write(const char *buffer, int32_t size, int32_t count, void *handle) {
return (int32_t)fwrite(buffer, (size_t)size, (size_t)count, (FILE *)handle);
}
int32_t bitlogger_file_flush(void *handle) {
return fflush((FILE *)handle);
}
int32_t bitlogger_file_close(void *handle) {
return fclose((FILE *)handle);
}
int32_t bitlogger_file_seek(void *handle, int32_t offset, int32_t origin) {
return fseek((FILE *)handle, offset, origin);
}
int64_t bitlogger_file_tell_i64(void *handle) {
#if defined(_WIN32)
__int64 position = _ftelli64((FILE *)handle);
#else
long long position = ftello((FILE *)handle);
#endif
return position < 0 ? -1 : (int64_t)position;
}
int32_t bitlogger_file_rename(const char *from_path, const char *to_path) {
return rename(from_path, to_path);
}
int32_t bitlogger_file_remove(const char *path) {
return remove(path);
}