#[cfg(test)]
mod parser_tests {
use crate::ast::Value;
use crate::parser::Parser;
#[test]
fn test_parser_exact_element_state() {
let input = r#"Button(id="btn", active=true, color=#ff0000, pad=10.5) "Click Me""#;
let mut parser = Parser::new(input);
parser.parse_all().expect("Парсинг не должен падать");
let m = &parser.module;
assert_eq!(
m.elem_types.len(),
1,
"Должен быть 1 элемент с inline-контентом"
);
assert_eq!(m.elem_prop_spans.len(), 1);
assert_eq!(m.elem_child_spans.len(), 1);
assert_eq!(m.elem_content.len(), 1);
assert_eq!(m.elem_types[0], "Button");
let (p_start, p_len) = m.elem_prop_spans[0];
assert_eq!(p_start, 0);
assert_eq!(p_len, 4);
let expected_keys = vec!["id", "active", "color", "pad"];
let expected_vals = vec![
Value::String("btn".to_string()),
Value::Bool(true),
Value::Color("#ff0000".to_string()),
Value::Float(10.5),
];
for i in 0..4 {
let idx = (p_start + i) as usize;
assert_eq!(
m.prop_keys[idx], expected_keys[i as usize],
"Ключ {} не совпал",
i
);
assert_eq!(
m.prop_values[idx], expected_vals[i as usize],
"Значение {} не совпало",
i
);
}
assert_eq!(
m.elem_content[0],
Some(Value::String("Click Me".to_string())),
"Контент текста потерян или искажен"
);
}
#[test]
fn test_parser_hierarchy_spans() {
let input = r#"
Root {
ChildA {}
ChildB { SubChild {} }
}
"#;
let mut parser = Parser::new(input);
parser.parse_all().expect("Парсинг должен пройти");
let m = &parser.module;
let root_idx = m.elem_types.iter().position(|t| t == "Root").unwrap();
let a_idx = m.elem_types.iter().position(|t| t == "ChildA").unwrap();
let b_idx = m.elem_types.iter().position(|t| t == "ChildB").unwrap();
let root_span = m.elem_child_spans[root_idx];
assert_eq!(root_span.1, 2, "У Root должно быть ровно 2 ребенка");
let child1 = &m.hierarchy[(root_span.0) as usize];
let child2 = &m.hierarchy[(root_span.0 + 1) as usize];
match (child1, child2) {
(crate::ast::NodeId::Element(id1), crate::ast::NodeId::Element(id2)) => {
assert_eq!(*id1 as usize, a_idx);
assert_eq!(*id2 as usize, b_idx);
}
_ => panic!("Дети Root должны быть элементами"),
}
}
}
#[cfg(test)]
mod compiler_tests {
use crate::compiler::Compiler;
use crate::opcodes::*;
use crate::parser::Parser;
struct BytecodeBuilder {
buf: Vec<u8>,
}
impl BytecodeBuilder {
fn new() -> Self {
let mut b = Self { buf: Vec::new() };
b.buf.extend_from_slice(&MAGIC_HEADER);
b
}
fn push_u8(mut self, val: u8) -> Self {
self.buf.push(val);
self
}
fn push_str(mut self, s: &str) -> Self {
let bytes = s.as_bytes();
self.buf
.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
self.buf.extend_from_slice(bytes);
self
}
fn push_i64(mut self, val: i64) -> Self {
self.buf.extend_from_slice(&val.to_le_bytes());
self
}
fn build(self) -> Vec<u8> {
self.buf
}
}
#[test]
fn test_compiler_exact_bytes_directive() {
let input = "@version 42";
let mut parser = Parser::new(input);
parser.parse_all().unwrap();
let root_nodes = vec![parser.module.hierarchy[0]];
let compiler = Compiler::new(&parser.module);
let bytecode = compiler.compile(&root_nodes);
let expected = BytecodeBuilder::new()
.push_u8(OP_VERSION)
.push_i64(42)
.build();
assert_eq!(bytecode, expected);
}
#[test]
fn test_compiler_exact_bytes_element() {
let input = r#"Box(id="main")"#;
let mut parser = Parser::new(input);
parser.parse_all().unwrap();
let root_nodes = vec![parser.module.hierarchy[0]];
let compiler = Compiler::new(&parser.module);
let bytecode = compiler.compile(&root_nodes);
let expected = BytecodeBuilder::new()
.push_u8(OP_ELEM_PUSH)
.push_str("Box")
.push_u8(OP_PROP_STR)
.push_str("id")
.push_str("main")
.push_u8(OP_ELEM_POP)
.build();
assert_eq!(bytecode, expected);
}
#[test]
fn test_compiler_rhei_zero_parsing() {
let input = r#"@global $net = !rhei: sys.network().status"#;
let mut parser = Parser::new(input);
parser.parse_all().unwrap();
let root_nodes = vec![parser.module.hierarchy[0]];
let compiler = Compiler::new(&parser.module);
let bytecode = compiler.compile(&root_nodes);
let expected = BytecodeBuilder::new()
.push_u8(OP_GLOBAL)
.push_str("net")
.push_u8(OP_PROP_RHEI)
.push_str("sys.network().status")
.build();
assert_eq!(bytecode, expected);
}
}
const TEST_GLINT_INPUT: &str = r#"
@version 1
@style "desktop.glts"
@global $username = !rhei: os.env("USER")
@global $hostname = !rhei: os.hostname()
@global $locale = "ru_RU"
@global $scaleFactor = 1.0
@singleton Config {
wallpaper = fs:/home/$username/.config/de/wallpaper.jpg
wallpaperFit = "cover"
iconTheme = "papirus-dark"
fontMain = "Inter"
fontMono = "JetBrains Mono"
fontSize = 13
workspaces = 4
animEnabled = true
accentColor = #cba6f7
taskbarPos = "bottom"
}
Screen(id="root", width=1920, height=1080, scale=$scaleFactor) {
Panel(id="taskbar", class="taskbar") {
Button(id="launcher-btn", class="launcher-button") {
Image(src=fs:/usr/share/glint-de/logo.svg) {}
}
}
}
"#;
#[test]
fn test_full_desktop_compilation() {
use crate::compiler::Compiler;
use crate::opcodes::MAGIC_HEADER;
use crate::parser::Parser;
let mut parser = Parser::new(TEST_GLINT_INPUT);
assert!(
parser.parse_all().is_ok(),
"Парсер вернул ошибку на валидном конфиге DE!"
);
let m = &parser.module;
assert!(!m.elem_types.is_empty(), "Не найдено ни одного элемента!");
assert_eq!(m.elem_types.len(), m.elem_prop_spans.len());
assert_eq!(m.elem_prop_spans.len(), m.elem_child_spans.len());
assert_eq!(m.prop_keys.len(), m.prop_values.len());
let mut child_indices = std::collections::HashSet::new();
for (start, len) in &m.elem_child_spans {
for i in *start..(*start + *len) {
child_indices.insert(i as usize);
}
}
let mut root_nodes = Vec::new();
for (i, node) in m.hierarchy.iter().enumerate() {
if !child_indices.contains(&i) {
root_nodes.push(node.clone());
}
}
let compiler = Compiler::new(m);
let bytecode = compiler.compile(&root_nodes);
assert_eq!(
&bytecode[0..4],
&MAGIC_HEADER,
"Отсутствует или поврежден GLBC заголовок"
);
assert!(bytecode.len() > 50, "Байткод подозрительно мал");
}
#[cfg(test)]
mod style_parser_tests {
use crate::ast::{Directive, ModuleSoA, NodeId, Value};
use crate::style_parser::StyleParser;
#[test]
fn test_style_parser_basic_rule() {
let input = "Screen { background: transparent; color: #fff }";
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().expect("Парсинг базового правила должен пройти успешно");
assert_eq!(m.hierarchy.len(), 1, "Должна быть одна директива");
assert_eq!(m.directives.len(), 1);
if let Directive::StyleRule { selector, prop_span } = &m.directives[0] {
assert_eq!(selector, "Screen");
assert_eq!(prop_span.1, 2, "Должно быть 2 свойства");
let start = prop_span.0 as usize;
assert_eq!(m.prop_keys[start], "background");
assert_eq!(m.prop_values[start], Value::Ident("transparent".to_string()));
assert_eq!(m.prop_keys[start + 1], "color");
assert_eq!(m.prop_values[start + 1], Value::Color("#fff".to_string()));
} else {
panic!("Ожидался узел StyleRule");
}
}
#[test]
fn test_style_parser_compile_time_variables() {
let input = r#"
$bg = #1e1e2e
$pad = 16px
Panel {
background: $bg
padding: $pad
color: $Config.textColor
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().expect("Парсинг с переменными");
if let Directive::StyleRule { prop_span, .. } = &m.directives[0] {
let start = prop_span.0 as usize;
assert_eq!(m.prop_values[start], Value::Color("#1e1e2e".to_string()));
assert_eq!(m.prop_values[start + 1], Value::Unit(16.0, "px".to_string()));
assert_eq!(m.prop_values[start + 2], Value::Variable("Config.textColor".to_string()));
} else {
panic!("Ожидался StyleRule");
}
}
#[test]
fn test_style_parser_mixins() {
let input = r#"
@mixin flex-center {
display: flex
align-items: center
}
Box {
@use flex-center
width: 100%
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
if let Directive::StyleRule { prop_span, .. } = &m.directives[0] {
assert_eq!(prop_span.1, 3, "Свойства из миксина должны добавиться к правилу");
let start = prop_span.0 as usize;
assert_eq!(m.prop_keys[start], "display");
assert_eq!(m.prop_values[start], Value::Ident("flex".to_string()));
assert_eq!(m.prop_keys[start + 1], "align-items");
assert_eq!(m.prop_values[start + 1], Value::Ident("center".to_string()));
assert_eq!(m.prop_keys[start + 2], "width");
assert_eq!(m.prop_values[start + 2], Value::Unit(100.0, "%".to_string()));
} else {
panic!("Ожидался StyleRule");
}
}
#[test]
fn test_style_parser_values_and_calls() {
let input = r#"
Overlay {
background: rgba(30, 30, 46, 0.88)
inset: 0 0 36px 0
box-shadow: 0 2px 8px rgba(0,0,0,0.45)
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
let start = match &m.directives[0] {
Directive::StyleRule { prop_span, .. } => prop_span.0 as usize,
_ => panic!("Ожидался StyleRule"),
};
assert_eq!(
m.prop_values[start],
Value::Call(
"rgba".to_string(),
vec![Value::Int(30), Value::Int(30), Value::Int(46), Value::Float(0.88)]
)
);
assert_eq!(
m.prop_values[start + 1],
Value::Array(vec![
Value::Int(0),
Value::Int(0),
Value::Unit(36.0, "px".to_string()),
Value::Int(0)
])
);
if let Value::Array(arr) = &m.prop_values[start + 2] {
assert_eq!(arr[0], Value::Int(0));
assert_eq!(arr[1], Value::Unit(2.0, "px".to_string()));
assert_eq!(arr[2], Value::Unit(8.0, "px".to_string()));
assert!(matches!(arr[3], Value::Call(..)));
} else {
panic!("box-shadow должен быть спарсен как массив (набор значений)");
}
}
#[test]
fn test_style_parser_animation() {
let input = r#"
@anim fade-in {
from { opacity: 0 }
to { opacity: 1 }
}
"#;
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
if let Directive::StyleAnim { name, frames } = &m.directives[0] {
assert_eq!(name, "fade-in");
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].0, "from");
assert_eq!(frames[1].0, "to");
} else {
panic!("Ожидалась директива StyleAnim");
}
}
}
#[cfg(test)]
mod style_compiler_tests {
use crate::ast::ModuleSoA;
use crate::compiler::Compiler;
use crate::opcodes::*;
use crate::style_parser::StyleParser;
struct BytecodeBuilder {
buf: Vec<u8>,
}
impl BytecodeBuilder {
fn new() -> Self {
let mut b = Self { buf: Vec::new() };
b.buf.extend_from_slice(&MAGIC_HEADER);
b
}
fn push_u8(mut self, val: u8) -> Self {
self.buf.push(val);
self
}
fn push_u32(mut self, val: u32) -> Self {
self.buf.extend_from_slice(&val.to_le_bytes());
self
}
fn push_str(mut self, s: &str) -> Self {
let bytes = s.as_bytes();
self.buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
self.buf.extend_from_slice(bytes);
self
}
fn push_f64(mut self, val: f64) -> Self {
self.buf.extend_from_slice(&val.to_le_bytes());
self
}
fn build(self) -> Vec<u8> {
self.buf
}
}
#[test]
fn test_compiler_style_exact_bytes() {
let input = "Box { padding: 8px; color: transparent }";
let mut m = ModuleSoA::new();
let mut parser = StyleParser::new(input, &mut m);
parser.parse_all().unwrap();
let compiler = Compiler::new(&m);
let bytecode = compiler.compile(&m.hierarchy);
let expected = BytecodeBuilder::new()
.push_u8(OP_STYLE_RULE)
.push_str("Box")
.push_u32(2)
.push_str("padding")
.push_u8(OP_PROP_UNIT)
.push_f64(8.0)
.push_str("px")
.push_str("color")
.push_u8(OP_PROP_IDENT)
.push_str("transparent")
.build();
assert_eq!(bytecode, expected);
}
}
#[cfg(test)]
mod integration_tests {
use crate::compile_project;
use crate::opcodes::MAGIC_HEADER;
const TEST_GLTS: &str = r#"
$accent = #cba6f7
@mixin flex-col {
display: flex
flex-direction: column
}
Panel.desktop {
@use flex-col
background: rgba(30, 30, 46, 0.88)
border: 1px solid $accent
}
"#;
const TEST_GLTM: &str = r#"
@version 1
Panel(id="main", class="desktop") {
Button(label="Start")
}
"#;
#[test]
fn test_full_compile_project() {
let result = compile_project(TEST_GLTM, TEST_GLTS);
assert!(result.is_ok(), "Проект должен успешно скомпилироваться: {:?}", result.err());
let bytecode = result.unwrap();
assert_eq!(
&bytecode[0..4],
&MAGIC_HEADER,
"Байткод должен начинаться с магического заголовка GLBC"
);
assert!(bytecode.len() > 100, "Байткод слишком мал, возможно скомпилировалось не все");
}
}