< Summary

Information
Class: Punch.CLI.PunchStorage
Assembly: punch
File(s): /home/runner/work/punch/punch/src/Punch.CLI/PunchStorage.cs
Line coverage
96%
Covered lines: 110
Uncovered lines: 4
Coverable lines: 114
Total lines: 168
Line coverage: 96.4%
Branch coverage
88%
Covered branches: 48
Total branches: 54
Branch coverage: 88.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_DataDirectoryOverride()100%11100%
GetDataDirectory()50%2280%
GetFilePath(...)100%11100%
GetDisplayPath(...)100%22100%
GetTicketsFilePath()50%44100%
LoadTickets()100%1212100%
GetSettingsFilePath()50%44100%
LoadSettings()83.33%66100%
Load(...)100%242491.89%
Save(...)100%11100%

File(s)

/home/runner/work/punch/punch/src/Punch.CLI/PunchStorage.cs

#LineLine coverage
 1using System.Text.Json;
 2
 3namespace Punch.CLI;
 4
 5internal static class PunchStorage
 6{
 5807    internal static string? DataDirectoryOverride { get; set; }
 8
 9    public static string GetDataDirectory()
 17710    {
 17711        if (!string.IsNullOrEmpty(DataDirectoryOverride))
 17712            return DataDirectoryOverride;
 013        return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".punch", "data");
 17714    }
 15
 16    public static string GetFilePath(DateOnly date)
 5217    {
 5218        return Path.Combine(GetDataDirectory(), $"{date:yyyy-MM-dd}.json");
 5219    }
 20
 21    public static string GetDisplayPath(DateOnly date)
 522    {
 523        var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
 524        var full = GetFilePath(date);
 525        return full.StartsWith(home) ? "~" + full[home.Length..] : full;
 526    }
 27
 28    // The manually-maintained tickets list sits alongside the data dir, e.g.
 29    // ~/.punch/tickets.txt (parent of ~/.punch/data).
 30    public static string GetTicketsFilePath()
 9431    {
 9432        var dataDir = GetDataDirectory();
 9433        var baseDir = Directory.GetParent(dataDir)?.FullName ?? dataDir;
 9434        return Path.Combine(baseDir, "tickets.txt");
 9435    }
 36
 37    // Loads the tickets list: one ticket per line, tab- or comma-delimited into
 38    // "ticket<sep>title". Blank lines and '#' comments are skipped, as are rows
 39    // with an empty ticket. Returns an empty list if the file is missing.
 40    public static List<TicketEntry> LoadTickets()
 1541    {
 1542        var path = GetTicketsFilePath();
 1543        if (!File.Exists(path))
 344            return new List<TicketEntry>();
 45
 1246        var result = new List<TicketEntry>();
 8647        foreach (var raw in File.ReadAllLines(path))
 2548        {
 2549            var line = raw.Trim();
 2550            if (line.Length == 0 || line.StartsWith('#'))
 451                continue;
 52
 2153            var parts = line.Split(new[] { '\t', ',' }, 2);
 2154            var ticket = parts[0].Trim();
 2155            if (ticket.Length == 0)
 156                continue;
 57
 2058            var title = parts.Length > 1 ? parts[1].Trim() : "";
 2059            result.Add(new TicketEntry(ticket, title));
 2060        }
 1261        return result;
 1562    }
 63
 64    // The settings file sits alongside the data dir, e.g. ~/.punch/settings.json
 65    // (parent of ~/.punch/data).
 66    public static string GetSettingsFilePath()
 1767    {
 1768        var dataDir = GetDataDirectory();
 1769        var baseDir = Directory.GetParent(dataDir)?.FullName ?? dataDir;
 1770        return Path.Combine(baseDir, "settings.json");
 1771    }
 72
 73    // Loads user settings from settings.json. Returns defaults if the file is
 74    // missing or cannot be parsed. TargetHours is clamped to a minimum of 1 to
 75    // avoid a divide-by-zero when computing the workday percentage.
 76    public static PunchSettings LoadSettings()
 877    {
 878        var path = GetSettingsFilePath();
 879        if (!File.Exists(path))
 180            return new PunchSettings();
 81
 82        try
 783        {
 784            var json = File.ReadAllText(path);
 785            var settings = JsonSerializer.Deserialize<PunchSettings>(json,
 786                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new PunchSettings();
 687            if (settings.TargetHours < 1)
 188                settings.TargetHours = 1;
 689            return settings;
 90        }
 191        catch (JsonException)
 192        {
 193            return new PunchSettings();
 94        }
 895    }
 96
 97    public static List<TimeBlock> Load(DateOnly date)
 2098    {
 2099        var path = GetFilePath(date);
 20100        if (!File.Exists(path))
 2101            return new List<TimeBlock>();
 102
 103        try
 18104        {
 18105            var json = File.ReadAllText(path);
 18106            var data = JsonSerializer.Deserialize<PunchData>(json);
 14107            if (data?.Blocks == null)
 2108                return new List<TimeBlock>();
 109
 12110            var result = new List<TimeBlock>();
 12111            var occupied = new bool[96];
 70112            foreach (var dto in data.Blocks)
 17113            {
 17114                if (dto.StartSlot < 0 || dto.StartSlot >= 96 || dto.Length < 1 || dto.StartSlot + dto.Length > 96)
 4115                    continue;
 116
 13117                var overlaps = false;
 130118                for (var s = dto.StartSlot; s < dto.StartSlot + dto.Length; s++)
 53119                {
 56120                    if (occupied[s]) { overlaps = true; break; }
 52121                }
 14122                if (overlaps) continue;
 123
 128124                for (var s = dto.StartSlot; s < dto.StartSlot + dto.Length; s++)
 52125                    occupied[s] = true;
 126
 12127                result.Add(new TimeBlock(dto.StartSlot, dto.Length, dto.Label, dto.Ticket));
 12128            }
 12129            return result;
 130        }
 4131        catch (JsonException ex)
 4132        {
 4133            var backupPath = path + ".bak";
 134            try
 4135            {
 4136                File.Copy(path, backupPath, overwrite: true);
 4137            }
 0138            catch (Exception backupEx)
 0139            {
 0140                throw new InvalidOperationException($"The daily log file is corrupted and could not be backed up ({backu
 141            }
 4142            throw new InvalidOperationException($"The daily log file is corrupted. A backup has been created at '{backup
 143        }
 16144    }
 145
 146    public static void Save(DateOnly date, IReadOnlyList<TimeBlock> blocks)
 14147    {
 14148        var dir = GetDataDirectory();
 14149        Directory.CreateDirectory(dir);
 150
 14151        var data = new PunchData
 14152        {
 15153            Blocks = blocks.Select(b => new TimeBlockDto
 15154            {
 15155                StartSlot = b.StartSlot,
 15156                Length = b.Length,
 15157                Label = b.Label,
 15158                Ticket = b.Ticket
 15159            }).ToList()
 14160        };
 161
 14162        var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
 14163        var path = GetFilePath(date);
 14164        var tmpPath = path + ".tmp";
 14165        File.WriteAllText(tmpPath, json);
 14166        File.Move(tmpPath, path, overwrite: true);
 14167    }
 168}