Skip to main content

oxihuman_core/
sub_task.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Sub-task tracking: decompose a parent task into named sub-tasks with progress.
6
7use std::collections::HashMap;
8
9/// Status of a sub-task.
10#[allow(dead_code)]
11#[derive(Debug, Clone, PartialEq)]
12pub enum SubTaskStatus {
13    Pending,
14    Running,
15    Done,
16    Failed(String),
17    Skipped,
18}
19
20/// A single sub-task entry.
21#[allow(dead_code)]
22#[derive(Debug, Clone)]
23pub struct SubTask {
24    pub name: String,
25    pub status: SubTaskStatus,
26    pub progress: f32,
27    pub weight: f32,
28}
29
30/// A collection of sub-tasks belonging to a parent task.
31#[allow(dead_code)]
32pub struct SubTaskSet {
33    tasks: Vec<SubTask>,
34    name_index: HashMap<String, usize>,
35}
36
37#[allow(dead_code)]
38impl SubTaskSet {
39    pub fn new() -> Self {
40        Self {
41            tasks: Vec::new(),
42            name_index: HashMap::new(),
43        }
44    }
45
46    pub fn add(&mut self, name: &str, weight: f32) {
47        let idx = self.tasks.len();
48        self.tasks.push(SubTask {
49            name: name.to_string(),
50            status: SubTaskStatus::Pending,
51            progress: 0.0,
52            weight: weight.max(0.0),
53        });
54        self.name_index.insert(name.to_string(), idx);
55    }
56
57    pub fn set_running(&mut self, name: &str) -> bool {
58        if let Some(&idx) = self.name_index.get(name) {
59            self.tasks[idx].status = SubTaskStatus::Running;
60            true
61        } else {
62            false
63        }
64    }
65
66    pub fn set_progress(&mut self, name: &str, progress: f32) -> bool {
67        if let Some(&idx) = self.name_index.get(name) {
68            self.tasks[idx].progress = progress.clamp(0.0, 1.0);
69            true
70        } else {
71            false
72        }
73    }
74
75    pub fn set_done(&mut self, name: &str) -> bool {
76        if let Some(&idx) = self.name_index.get(name) {
77            self.tasks[idx].status = SubTaskStatus::Done;
78            self.tasks[idx].progress = 1.0;
79            true
80        } else {
81            false
82        }
83    }
84
85    pub fn set_failed(&mut self, name: &str, reason: &str) -> bool {
86        if let Some(&idx) = self.name_index.get(name) {
87            self.tasks[idx].status = SubTaskStatus::Failed(reason.to_string());
88            true
89        } else {
90            false
91        }
92    }
93
94    pub fn set_skipped(&mut self, name: &str) -> bool {
95        if let Some(&idx) = self.name_index.get(name) {
96            self.tasks[idx].status = SubTaskStatus::Skipped;
97            true
98        } else {
99            false
100        }
101    }
102
103    pub fn get(&self, name: &str) -> Option<&SubTask> {
104        self.name_index.get(name).map(|&i| &self.tasks[i])
105    }
106
107    /// Weighted overall progress (0.0..=1.0).
108    pub fn overall_progress(&self) -> f32 {
109        let total_weight: f32 = self.tasks.iter().map(|t| t.weight).sum();
110        if total_weight <= 0.0 {
111            return 0.0;
112        }
113        let weighted: f32 = self.tasks.iter().map(|t| t.progress * t.weight).sum();
114        (weighted / total_weight).clamp(0.0, 1.0)
115    }
116
117    pub fn done_count(&self) -> usize {
118        self.tasks
119            .iter()
120            .filter(|t| t.status == SubTaskStatus::Done)
121            .count()
122    }
123
124    pub fn failed_count(&self) -> usize {
125        self.tasks
126            .iter()
127            .filter(|t| matches!(t.status, SubTaskStatus::Failed(_)))
128            .count()
129    }
130
131    pub fn pending_count(&self) -> usize {
132        self.tasks
133            .iter()
134            .filter(|t| t.status == SubTaskStatus::Pending)
135            .count()
136    }
137
138    pub fn task_count(&self) -> usize {
139        self.tasks.len()
140    }
141
142    pub fn all_done(&self) -> bool {
143        !self.tasks.is_empty()
144            && self
145                .tasks
146                .iter()
147                .all(|t| matches!(t.status, SubTaskStatus::Done | SubTaskStatus::Skipped))
148    }
149
150    pub fn has_failures(&self) -> bool {
151        self.tasks
152            .iter()
153            .any(|t| matches!(t.status, SubTaskStatus::Failed(_)))
154    }
155}
156
157impl Default for SubTaskSet {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163pub fn new_sub_task_set() -> SubTaskSet {
164    SubTaskSet::new()
165}
166
167pub fn sts_add(set: &mut SubTaskSet, name: &str, weight: f32) {
168    set.add(name, weight);
169}
170
171pub fn sts_done(set: &mut SubTaskSet, name: &str) -> bool {
172    set.set_done(name)
173}
174
175pub fn sts_failed(set: &mut SubTaskSet, name: &str, reason: &str) -> bool {
176    set.set_failed(name, reason)
177}
178
179pub fn sts_overall(set: &SubTaskSet) -> f32 {
180    set.overall_progress()
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn empty_set() {
189        let s = new_sub_task_set();
190        assert_eq!(s.task_count(), 0);
191    }
192
193    #[test]
194    fn add_and_get() {
195        let mut s = new_sub_task_set();
196        sts_add(&mut s, "load", 1.0);
197        assert!(s.get("load").is_some());
198        assert_eq!(
199            s.get("load").expect("should succeed").status,
200            SubTaskStatus::Pending
201        );
202    }
203
204    #[test]
205    fn set_running() {
206        let mut s = new_sub_task_set();
207        sts_add(&mut s, "process", 1.0);
208        assert!(s.set_running("process"));
209        assert_eq!(
210            s.get("process").expect("should succeed").status,
211            SubTaskStatus::Running
212        );
213    }
214
215    #[test]
216    fn done_count() {
217        let mut s = new_sub_task_set();
218        sts_add(&mut s, "a", 1.0);
219        sts_add(&mut s, "b", 1.0);
220        sts_done(&mut s, "a");
221        assert_eq!(s.done_count(), 1);
222    }
223
224    #[test]
225    fn failed_count() {
226        let mut s = new_sub_task_set();
227        sts_add(&mut s, "task", 1.0);
228        sts_failed(&mut s, "task", "timeout");
229        assert_eq!(s.failed_count(), 1);
230        assert!(s.has_failures());
231    }
232
233    #[test]
234    fn overall_progress_even_weights() {
235        let mut s = new_sub_task_set();
236        sts_add(&mut s, "a", 1.0);
237        sts_add(&mut s, "b", 1.0);
238        sts_done(&mut s, "a");
239        let p = sts_overall(&s);
240        assert!((p - 0.5).abs() < 1e-6);
241    }
242
243    #[test]
244    fn all_done_when_all_completed() {
245        let mut s = new_sub_task_set();
246        sts_add(&mut s, "x", 1.0);
247        sts_done(&mut s, "x");
248        assert!(s.all_done());
249    }
250
251    #[test]
252    fn skipped_counts_as_done_for_all_done() {
253        let mut s = new_sub_task_set();
254        sts_add(&mut s, "opt", 0.5);
255        s.set_skipped("opt");
256        assert!(s.all_done());
257    }
258
259    #[test]
260    fn set_progress_clamps() {
261        let mut s = new_sub_task_set();
262        sts_add(&mut s, "t", 1.0);
263        s.set_progress("t", 2.0);
264        assert!((s.get("t").expect("should succeed").progress - 1.0).abs() < 1e-6);
265    }
266
267    #[test]
268    fn missing_task_returns_false() {
269        let mut s = new_sub_task_set();
270        assert!(!sts_done(&mut s, "ghost"));
271    }
272}