Laufzeit-Eingabe und Zonenzustand korrigieren und Change archivieren
This commit is contained in:
@@ -405,7 +405,7 @@ fn run_chain(
|
||||
}
|
||||
vm.rt.command.clone_from(&command);
|
||||
vm.set_poll_interrupt(true);
|
||||
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
|
||||
if !vm.rt.zeitpunkt().1 {
|
||||
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
|
||||
}
|
||||
let event = vm.run(host);
|
||||
|
||||
@@ -63,11 +63,15 @@ fn screen_groessen(name: &str, src: &str) -> Vec<(usize, usize)> {
|
||||
wert.split(',').map(|w| groesse_lesen(name, w)).collect()
|
||||
}
|
||||
|
||||
/// Optionale Zeitzone `' tb-tz: ±HH:MM`. Ohne Angabe gilt die Zone des
|
||||
/// Optionale Zeitzone `' tb-tz: ±HH:MM` oder `unknown` für UTC-Rückfall.
|
||||
/// Ohne Angabe gilt die Zone des
|
||||
/// Rechners — Zeitprogramme im Korpus setzen sie, damit ihre Sollausgabe
|
||||
/// unabhängig vom Standort des Testrechners bleibt.
|
||||
fn tz_direktive(name: &str, src: &str) -> Option<tb_runtime::datetime::Zeitzone> {
|
||||
let wert = direktive(src, "tb-tz:")?;
|
||||
if wert == "unknown" {
|
||||
return Some(tb_runtime::datetime::Zeitzone::Unbekannt);
|
||||
}
|
||||
let (vorzeichen, rest) = match wert.strip_prefix('-') {
|
||||
Some(r) => (-1i32, r),
|
||||
None => (1i32, wert.trim_start_matches('+')),
|
||||
|
||||
@@ -340,6 +340,7 @@ pub enum Builtin {
|
||||
DateSet,
|
||||
TimeSet,
|
||||
Now,
|
||||
TimezoneKnown,
|
||||
DateSerial,
|
||||
TimeSerial,
|
||||
DateValue,
|
||||
|
||||
@@ -243,6 +243,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
|
||||
"DATE$" | "TIME$" => (0, 0, &[], St),
|
||||
"TIMER" => (0, 0, &[], Sg),
|
||||
"NOW" => (0, 0, &[], Db),
|
||||
"TIMEZONEKNOWN" => (0, 0, &[], I),
|
||||
"DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db),
|
||||
"DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db),
|
||||
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I),
|
||||
@@ -5374,6 +5375,7 @@ impl Sema {
|
||||
b(bt, a, Ty::Dbl, self)
|
||||
}
|
||||
"NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
|
||||
"TIMEZONEKNOWN" => b(Builtin::TimezoneKnown, vec![], Ty::Int, self),
|
||||
"DATESERIAL" | "TIMESERIAL" => {
|
||||
let x = conv_arg!(0, NumTy::Lng);
|
||||
let y = conv_arg!(1, NumTy::Lng);
|
||||
@@ -5663,6 +5665,19 @@ mod tests {
|
||||
assert!(diags("PRINT CHR$(\"x\")").contains(&"Type mismatch".to_string()));
|
||||
assert!(diags("x$ = INKEY$").is_empty());
|
||||
assert!(diags("t! = TIMER").is_empty());
|
||||
assert!(matches!(
|
||||
super::builtin_fn("TIMEZONEKNOWN"),
|
||||
Some((0, 0, [], super::RetK::I))
|
||||
));
|
||||
assert!(diags("PRINT TIMEZONEKNOWN").is_empty());
|
||||
for name in ["TIMEZONEKNOWN(1)", "MKL$()"] {
|
||||
let d = diags(&format!("PRINT {name}"));
|
||||
assert!(
|
||||
d.iter().any(|m| m.contains("Argument-count mismatch")
|
||||
&& m.contains(name.split('(').next().unwrap())),
|
||||
"{d:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -30,6 +30,8 @@ pub struct RtState {
|
||||
pub print: PrintState,
|
||||
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
|
||||
pub eingaben: VecDeque<Eingabe>,
|
||||
/// Bereits angenommener Rest einer von INPUT$ geteilten Taste.
|
||||
tastenrest: String,
|
||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||
pub abbruch: bool,
|
||||
/// Eingabeende wurde gemeldet.
|
||||
@@ -52,8 +54,10 @@ pub struct RtState {
|
||||
/// Versatz (dokumentierte Abweichung). Er wirkt auf die Ortszeit.
|
||||
pub uhr_offset: i64,
|
||||
/// Woher der UTC-Versatz kommt. Vorgabe ist die Zone des Rechners;
|
||||
/// ist sie nicht ermittelbar, steht hier `Unbekannt` und es gilt UTC.
|
||||
/// bei fehlgeschlagener Auflösung gilt für diese Abfrage UTC.
|
||||
pub zeitzone: dt::Zeitzone,
|
||||
/// Auflösung der Systemzone; durch Einbetter auch ohne Systemänderung ersetzbar.
|
||||
pub zonen_ermittler: fn(i64) -> Option<i32>,
|
||||
rng: u32,
|
||||
rnd_last: f32,
|
||||
pub command: String,
|
||||
@@ -85,6 +89,7 @@ impl Default for RtState {
|
||||
isam: Default::default(),
|
||||
print: PrintState::default(),
|
||||
eingaben: VecDeque::new(),
|
||||
tastenrest: String::new(),
|
||||
abbruch: false,
|
||||
ende: false,
|
||||
// Das Vorbild belegt die Makros im Interpreter vor; ein
|
||||
@@ -97,7 +102,8 @@ impl Default for RtState {
|
||||
dir_treffer: Vec::new(),
|
||||
dir_index: 0,
|
||||
uhr_offset: 0,
|
||||
zeitzone: dt::Zeitzone::ermitteln(),
|
||||
zeitzone: dt::Zeitzone::System,
|
||||
zonen_ermittler: dt::utc_versatz,
|
||||
// Startzustand des Vorbild-PRNG; die exakte
|
||||
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
|
||||
// („RND/RANDOMIZE — kompatibler PRNG").
|
||||
@@ -110,6 +116,32 @@ impl Default for RtState {
|
||||
}
|
||||
|
||||
impl RtState {
|
||||
/// Ortszeit und Gültigkeit stammen aus derselben aktuellen Zonenauflösung.
|
||||
pub fn zeitpunkt(&self) -> (i64, bool) {
|
||||
let utc = dt::jetzt_utc();
|
||||
let zone = match self.zeitzone {
|
||||
dt::Zeitzone::System => (self.zonen_ermittler)(utc),
|
||||
dt::Zeitzone::Fest(n) => Some(n),
|
||||
dt::Zeitzone::Unbekannt => None,
|
||||
};
|
||||
(
|
||||
utc + i64::from(zone.unwrap_or(0)) + self.uhr_offset,
|
||||
zone.is_some(),
|
||||
)
|
||||
}
|
||||
|
||||
fn jetzt_serial(&self) -> f64 {
|
||||
self.zeitpunkt().0 as f64 / 86_400.0 + dt::serial_aus_ymd(1970, 1, 1)
|
||||
}
|
||||
|
||||
/// Höchstens n Codepoints annehmen, übrige Zeichen vor der nächsten Taste lesen.
|
||||
pub fn tastenanteil(&mut self, mut taste: String, n: usize) -> String {
|
||||
if let Some((ende, _)) = taste.char_indices().nth(n) {
|
||||
self.tastenrest = taste.split_off(ende);
|
||||
}
|
||||
taste
|
||||
}
|
||||
|
||||
/// Ereignisse vom Host abholen und einsortieren: Größenänderungen wirken
|
||||
/// sofort auf den Bildschirm, Abbruch und Eingabeende werden gemerkt,
|
||||
/// Tastendrücke landen in der Warteschlange für `INKEY$`/`INPUT`.
|
||||
@@ -207,6 +239,9 @@ impl RtState {
|
||||
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
|
||||
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
|
||||
pub fn gepufferte_taste(&mut self) -> Option<String> {
|
||||
if !self.tastenrest.is_empty() {
|
||||
return Some(std::mem::take(&mut self.tastenrest));
|
||||
}
|
||||
while let Some(i) = self
|
||||
.eingaben
|
||||
.iter()
|
||||
@@ -496,7 +531,8 @@ pub mod ids {
|
||||
pub const GRAPHICS_LINE: u16 = 153;
|
||||
pub const GRAPHICS_PAINT: u16 = 154;
|
||||
pub const GRAPHICS_VIEW: u16 = 155;
|
||||
pub const COUNT: u16 = 156;
|
||||
pub const TIMEZONEKNOWN: u16 = 156;
|
||||
pub const COUNT: u16 = 157;
|
||||
}
|
||||
|
||||
/// Dispatch-Tabelle in Index-Reihenfolge.
|
||||
@@ -659,6 +695,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
bi_graphics_line,
|
||||
bi_graphics_paint,
|
||||
bi_graphics_view,
|
||||
bi_timezoneknown,
|
||||
];
|
||||
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
|
||||
TABLE
|
||||
@@ -1470,7 +1507,9 @@ fn bi_seek_stmt(
|
||||
if pos < 1 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
st.dateien.get(arg_i32(a, 0)?)?.position = pos as u64;
|
||||
st.dateien
|
||||
.get(arg_i32(a, 0)?)?
|
||||
.position_setzen(pos as u64)?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1706,9 +1745,9 @@ fn bi_cv(
|
||||
};
|
||||
let bytes: Vec<u8> = text
|
||||
.chars()
|
||||
.map(|c| u8::try_from(c as u32).unwrap_or(0))
|
||||
.collect();
|
||||
if bytes.len() < breite {
|
||||
.map(|c| u8::try_from(c as u32).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL))
|
||||
.collect::<Result<_, _>>()?;
|
||||
if bytes.len() != breite {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
Ok(Some(match art {
|
||||
@@ -2180,18 +2219,18 @@ fn bi_input_s(
|
||||
if a.len() > 1 {
|
||||
let nummer = arg_i32(a, 1)?;
|
||||
let datei = st.dateien.get(nummer)?;
|
||||
let bytes = datei.bytes_lesen(datei.position, n as usize)?;
|
||||
return Ok(Some(Value::Str(Rc::from(
|
||||
String::from_utf8_lossy(&bytes).as_ref(),
|
||||
))));
|
||||
return s_ok(datei.zeichen_lesen(n as usize)?);
|
||||
}
|
||||
// Blockierend, ohne Echo auf dem Bildschirm.
|
||||
let mut s = String::new();
|
||||
for _ in 0..n {
|
||||
let mut rest = n as usize;
|
||||
while rest > 0 {
|
||||
host.present(&st.screen);
|
||||
let Some(t) = st.naechste_taste(host, true) else {
|
||||
return Err(RuntimeError(62)); // Input past end of file
|
||||
};
|
||||
let t = st.tastenanteil(t, rest);
|
||||
rest -= t.chars().count();
|
||||
s.push_str(&t);
|
||||
}
|
||||
Ok(Some(Value::Str(Rc::from(s.as_str()))))
|
||||
@@ -2199,6 +2238,14 @@ fn bi_input_s(
|
||||
|
||||
// ---- Sonstiges ------------------------------------------------------------------
|
||||
|
||||
fn bi_timezoneknown(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Int(if st.zeitpunkt().1 { -1 } else { 0 })))
|
||||
}
|
||||
|
||||
fn bi_timer(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
@@ -2209,7 +2256,7 @@ fn bi_timer(
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs_f64().fract())
|
||||
.unwrap_or(0.0);
|
||||
let ganze = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
|
||||
let ganze = st.zeitpunkt().0.rem_euclid(86_400);
|
||||
Ok(Some(Value::Sng(ganze as f32 + bruchteil as f32)))
|
||||
}
|
||||
|
||||
@@ -2218,7 +2265,7 @@ fn bi_date(
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let secs = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset);
|
||||
let secs = st.zeitpunkt().0;
|
||||
let (y, m, d) = dt::civil_from_days(secs.div_euclid(86_400));
|
||||
s_ok(format!("{m:02}-{d:02}-{y:04}"))
|
||||
}
|
||||
@@ -2228,7 +2275,7 @@ fn bi_time(
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let s = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
|
||||
let s = st.zeitpunkt().0.rem_euclid(86_400);
|
||||
s_ok(format!(
|
||||
"{:02}:{:02}:{:02}",
|
||||
s / 3600,
|
||||
@@ -2245,7 +2292,7 @@ fn bi_date_set(
|
||||
let t = arg_str(a, 0)?;
|
||||
let ziel = dt::datevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
// Nur den Datumsanteil verschieben, die Tageszeit bleibt.
|
||||
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
|
||||
let jetzt = st.jetzt_serial();
|
||||
let diff_tage = ziel.floor() - jetzt.floor();
|
||||
st.uhr_offset += (diff_tage * 86_400.0) as i64;
|
||||
Ok(None)
|
||||
@@ -2258,7 +2305,7 @@ fn bi_time_set(
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let t = arg_str(a, 0)?;
|
||||
let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
|
||||
let jetzt = st.jetzt_serial();
|
||||
let diff = ziel - (jetzt - jetzt.floor());
|
||||
st.uhr_offset += (diff * 86_400.0).round() as i64;
|
||||
Ok(None)
|
||||
@@ -2269,10 +2316,7 @@ fn bi_now(
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Dbl(dt::jetzt_serial(
|
||||
st.zeitzone,
|
||||
st.uhr_offset,
|
||||
))))
|
||||
Ok(Some(Value::Dbl(st.jetzt_serial())))
|
||||
}
|
||||
|
||||
fn bi_date_serial(
|
||||
@@ -2762,6 +2806,72 @@ mod tests {
|
||||
Value::Str(Rc::from(v))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_s_dispatch_bewahrt_codepointreste_auch_ohne_vm() {
|
||||
let mut st = RtState::default();
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste("\0;".into(), 0));
|
||||
host.ereignis(Ereignis::Taste("ä😀".into(), 0));
|
||||
for (n, expected) in [(0, ""), (1, "\0"), (2, ";ä"), (1, "😀")] {
|
||||
let got = bi_input_s(&mut st, &mut host, &mut [Value::Int(n)]).unwrap();
|
||||
assert!(matches!(got, Some(Value::Str(s)) if &*s == expected));
|
||||
}
|
||||
host.ereignis(Ereignis::Ende);
|
||||
assert_eq!(
|
||||
bi_input_s(&mut st, &mut host, &mut [Value::Int(1)]).unwrap_err(),
|
||||
RuntimeError(62)
|
||||
);
|
||||
assert_eq!(
|
||||
bi_input_s(&mut st, &mut host, &mut [Value::Int(-1)]).unwrap_err(),
|
||||
RuntimeError(5)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen() {
|
||||
thread_local! {
|
||||
static ZONE: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
let mut st = RtState {
|
||||
zonen_ermittler: |_| ZONE.get(),
|
||||
uhr_offset: 4321,
|
||||
..RtState::default()
|
||||
};
|
||||
let mut host = CaptureHost::default();
|
||||
for zone in [None, Some(7200), None, Some(0)] {
|
||||
ZONE.set(zone);
|
||||
let before = dt::jetzt_utc();
|
||||
let (time, known) = st.zeitpunkt();
|
||||
assert_eq!(known, zone.is_some());
|
||||
let shift = i64::from(zone.unwrap_or(0)) + st.uhr_offset;
|
||||
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&time));
|
||||
assert!(
|
||||
matches!(bi_timezoneknown(&mut st, &mut host, &mut []).unwrap(), Some(Value::Int(n)) if n == if known { -1 } else { 0 })
|
||||
);
|
||||
let Some(Value::Dbl(now)) = bi_now(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let seconds = ((now - dt::serial_aus_ymd(1970, 1, 1)) * 86_400.0).round() as i64;
|
||||
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&seconds));
|
||||
let Some(Value::Str(date)) = bi_date(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let Some(Value::Str(clock)) = bi_time(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let date_time = ((dt::datevalue(&date).unwrap() + dt::timevalue(&clock).unwrap()
|
||||
- dt::serial_aus_ymd(1970, 1, 1))
|
||||
* 86_400.0)
|
||||
.round() as i64;
|
||||
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&date_time));
|
||||
let Some(Value::Sng(timer)) = bi_timer(&mut st, &mut host, &mut []).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert!((timer as f64 - seconds.rem_euclid(86_400) as f64).abs() < 2.0);
|
||||
assert_eq!(st.uhr_offset, 4321);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringfunktionen_randfaelle() {
|
||||
// VAL liest Präfix
|
||||
|
||||
@@ -132,8 +132,8 @@ pub enum Zeitzone {
|
||||
|
||||
impl Zeitzone {
|
||||
/// Versatz zu einem Zeitpunkt. Bei `System` ohne ermittelbare Zone
|
||||
/// fällt der Wert auf 0 (UTC) zurück — die Rückfallebene ist über
|
||||
/// [`Zeitzone::ermitteln`] erkennbar.
|
||||
/// fällt der Wert auf 0 (UTC) zurück. BASIC und Einbetter verwenden
|
||||
/// [`crate::builtins::RtState::zeitpunkt`] für Zeit und aktuellen Status.
|
||||
pub fn versatz(&self, zeitpunkt: i64) -> i32 {
|
||||
match self {
|
||||
Zeitzone::System => utc_versatz(zeitpunkt).unwrap_or(0),
|
||||
@@ -142,8 +142,9 @@ impl Zeitzone {
|
||||
}
|
||||
}
|
||||
|
||||
/// Einmal beim Start: `System`, wenn die Zone lesbar ist, sonst
|
||||
/// `Unbekannt`. Damit ist der Rückfall auf UTC sichtbar statt still.
|
||||
/// Momentaufnahme: `System`, wenn die Zone lesbar ist, sonst `Unbekannt`.
|
||||
/// Die Laufzeit behält `System` als Quelle, damit sie nach Ausfällen
|
||||
/// erneut auflösen kann; `RtState::zeitpunkt` liefert den aktuellen Status.
|
||||
pub fn ermitteln() -> Zeitzone {
|
||||
match utc_versatz(jetzt_utc()) {
|
||||
Some(_) => Zeitzone::System,
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct Feld {
|
||||
pub struct Datei {
|
||||
pub modus: Modus,
|
||||
pub pfad: PathBuf,
|
||||
datei: File,
|
||||
datei: BufReader<File>,
|
||||
/// Recordlänge (`RANDOM`); bei `BINARY` immer 1.
|
||||
pub reclen: usize,
|
||||
/// Recordpuffer für `FIELD`/`GET`/`PUT`.
|
||||
@@ -61,8 +61,8 @@ pub struct Datei {
|
||||
/// Nächste Position: Datensatz bei `RANDOM`, Byte bei `BINARY`,
|
||||
/// jeweils 1-basiert.
|
||||
pub position: u64,
|
||||
/// Zeilenpuffer für sequenzielles Lesen.
|
||||
leser: Option<BufReader<File>>,
|
||||
/// Ein Komma kann auch vor EOF noch ein leeres Feld eröffnen.
|
||||
leeres_feld: bool,
|
||||
}
|
||||
|
||||
impl Datei {
|
||||
@@ -82,11 +82,7 @@ impl Datei {
|
||||
opt.read(true).write(true).create(true);
|
||||
}
|
||||
}
|
||||
let datei = opt.open(pfad).map_err(fehler_aus_io)?;
|
||||
let leser = match modus {
|
||||
Modus::Input => Some(BufReader::new(File::open(pfad).map_err(fehler_aus_io)?)),
|
||||
_ => None,
|
||||
};
|
||||
let datei = BufReader::new(opt.open(pfad).map_err(|e| fehler_am_pfad(e, pfad))?);
|
||||
let reclen = if modus == Modus::Random {
|
||||
reclen.max(1)
|
||||
} else {
|
||||
@@ -100,13 +96,14 @@ impl Datei {
|
||||
puffer: vec![b' '; reclen],
|
||||
felder: Vec::new(),
|
||||
position: 1,
|
||||
leser,
|
||||
leeres_feld: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Dateigröße in Bytes.
|
||||
pub fn laenge(&self) -> Result<u64, RuntimeError> {
|
||||
self.datei
|
||||
.get_ref()
|
||||
.metadata()
|
||||
.map(|m| m.len())
|
||||
.map_err(fehler_aus_io)
|
||||
@@ -117,8 +114,7 @@ impl Datei {
|
||||
pub fn eof(&mut self) -> Result<bool, RuntimeError> {
|
||||
match self.modus {
|
||||
Modus::Input => {
|
||||
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
|
||||
Ok(l.fill_buf().map_err(fehler_aus_io)?.is_empty())
|
||||
Ok(!self.leeres_feld && self.datei.fill_buf().map_err(fehler_aus_io)?.is_empty())
|
||||
}
|
||||
Modus::Random => Ok((self.position - 1) * self.reclen as u64 >= self.laenge()?),
|
||||
Modus::Binary => Ok(self.position > self.laenge()?),
|
||||
@@ -132,24 +128,143 @@ impl Datei {
|
||||
if !matches!(self.modus, Modus::Output | Modus::Append) {
|
||||
return Err(RuntimeError(54)); // Bad file mode
|
||||
}
|
||||
self.datei.write_all(text.as_bytes()).map_err(fehler_aus_io)
|
||||
self.datei
|
||||
.get_mut()
|
||||
.write_all(text.as_bytes())
|
||||
.map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// Eine Zeile lesen (ohne Zeilenende); `None` = Dateiende.
|
||||
/// `SEEK`: auch den Lesepuffer verwerfen; Position bleibt 1-basiert.
|
||||
pub fn position_setzen(&mut self, pos: u64) -> Result<(), RuntimeError> {
|
||||
if pos == 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
if self.modus == Modus::Input {
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(pos - 1))
|
||||
.map_err(fehler_aus_io)?;
|
||||
}
|
||||
self.position = pos;
|
||||
self.leeres_feld = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn byte_lesen(&mut self) -> Result<Option<u8>, RuntimeError> {
|
||||
let mut b = [0];
|
||||
if self.datei.read(&mut b).map_err(fehler_aus_io)? == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
self.position += 1;
|
||||
self.leeres_feld = false;
|
||||
Ok(Some(b[0]))
|
||||
}
|
||||
|
||||
fn crlf_beenden(&mut self, b: u8) -> Result<(), RuntimeError> {
|
||||
if b == b'\r' && self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'\n') {
|
||||
self.byte_lesen()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Eine Zeile ab dem gemeinsamen Cursor, ohne genau ein Zeilenende.
|
||||
pub fn zeile_lesen(&mut self) -> Result<Option<String>, RuntimeError> {
|
||||
if self.modus != Modus::Input {
|
||||
return Err(RuntimeError(54));
|
||||
}
|
||||
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
|
||||
let mut roh = Vec::new();
|
||||
let n = l.read_until(b'\n', &mut roh).map_err(fehler_aus_io)?;
|
||||
if n == 0 {
|
||||
let mut gelesen = false;
|
||||
self.leeres_feld = false;
|
||||
while let Some(b) = self.byte_lesen()? {
|
||||
gelesen = true;
|
||||
if matches!(b, b'\r' | b'\n') {
|
||||
self.crlf_beenden(b)?;
|
||||
break;
|
||||
}
|
||||
roh.push(b);
|
||||
}
|
||||
if !gelesen {
|
||||
return Ok(None);
|
||||
}
|
||||
while roh.last() == Some(&b'\n') || roh.last() == Some(&b'\r') {
|
||||
roh.pop();
|
||||
String::from_utf8(roh)
|
||||
.map(Some)
|
||||
.map_err(|_| RuntimeError(57))
|
||||
}
|
||||
|
||||
/// Nächstes INPUT-#-Feld, ohne nachfolgende Felder oder Zeilen zu verbrauchen.
|
||||
pub fn feld_lesen(&mut self) -> Result<String, RuntimeError> {
|
||||
if self.modus != Modus::Input {
|
||||
return Err(RuntimeError(54));
|
||||
}
|
||||
Ok(Some(String::from_utf8_lossy(&roh).into_owned()))
|
||||
let mut roh = Vec::new();
|
||||
let mut in_quote = false;
|
||||
let mut hatte_quote = false;
|
||||
let mut gelesen = std::mem::take(&mut self.leeres_feld);
|
||||
while let Some(b) = self.byte_lesen()? {
|
||||
gelesen = true;
|
||||
match b {
|
||||
b'"' if !hatte_quote && roh.iter().all(u8::is_ascii_whitespace) => {
|
||||
roh.clear();
|
||||
in_quote = true;
|
||||
hatte_quote = true;
|
||||
}
|
||||
b'"' if in_quote => {
|
||||
if self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'"') {
|
||||
self.byte_lesen()?;
|
||||
roh.push(b'"');
|
||||
} else {
|
||||
in_quote = false;
|
||||
}
|
||||
}
|
||||
b',' if !in_quote => {
|
||||
self.leeres_feld = true;
|
||||
break;
|
||||
}
|
||||
b'\r' | b'\n' if !in_quote => {
|
||||
self.crlf_beenden(b)?;
|
||||
break;
|
||||
}
|
||||
b if hatte_quote && !in_quote && b.is_ascii_whitespace() => {}
|
||||
_ => roh.push(b),
|
||||
}
|
||||
}
|
||||
if !gelesen || in_quote {
|
||||
return Err(RuntimeError(62));
|
||||
}
|
||||
let text = String::from_utf8(roh).map_err(|_| RuntimeError(57))?;
|
||||
Ok(if hatte_quote {
|
||||
text
|
||||
} else {
|
||||
text.trim().to_owned()
|
||||
})
|
||||
}
|
||||
|
||||
/// INPUT$: exakt n UTF-8-Codepoints; beschädigte Daten sind Fehler 57.
|
||||
pub fn zeichen_lesen(&mut self, n: usize) -> Result<String, RuntimeError> {
|
||||
if !matches!(self.modus, Modus::Input | Modus::Binary) {
|
||||
return Err(RuntimeError(54));
|
||||
}
|
||||
if self.modus == Modus::Binary {
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(self.position - 1))
|
||||
.map_err(fehler_aus_io)?;
|
||||
}
|
||||
let mut text = String::new();
|
||||
for _ in 0..n {
|
||||
let mut bytes = [0; 4];
|
||||
bytes[0] = self.byte_lesen()?.ok_or(RuntimeError(62))?;
|
||||
let breite = match bytes[0] {
|
||||
0..=0x7f => 1,
|
||||
0xc2..=0xdf => 2,
|
||||
0xe0..=0xef => 3,
|
||||
0xf0..=0xf4 => 4,
|
||||
_ => return Err(RuntimeError(57)),
|
||||
};
|
||||
for b in &mut bytes[1..breite] {
|
||||
*b = self.byte_lesen()?.ok_or(RuntimeError(57))?;
|
||||
}
|
||||
text.push_str(std::str::from_utf8(&bytes[..breite]).map_err(|_| RuntimeError(57))?);
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
/// Datensatz `nr` (1-basiert) in den Puffer lesen.
|
||||
@@ -179,7 +294,10 @@ impl Datei {
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(fehler_aus_io)?;
|
||||
let puffer = std::mem::take(&mut self.puffer);
|
||||
self.datei.write_all(&puffer).map_err(fehler_aus_io)?;
|
||||
self.datei
|
||||
.get_mut()
|
||||
.write_all(&puffer)
|
||||
.map_err(fehler_aus_io)?;
|
||||
self.puffer = puffer;
|
||||
self.position = nr + 1;
|
||||
Ok(())
|
||||
@@ -211,13 +329,16 @@ impl Datei {
|
||||
self.datei
|
||||
.seek(SeekFrom::Start(pos - 1))
|
||||
.map_err(fehler_aus_io)?;
|
||||
self.datei.write_all(daten).map_err(fehler_aus_io)?;
|
||||
self.datei
|
||||
.get_mut()
|
||||
.write_all(daten)
|
||||
.map_err(fehler_aus_io)?;
|
||||
self.position = pos + daten.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schliessen(&mut self) -> Result<(), RuntimeError> {
|
||||
self.datei.flush().map_err(fehler_aus_io)
|
||||
self.datei.get_mut().flush().map_err(fehler_aus_io)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +364,6 @@ impl Dateien {
|
||||
return Err(RuntimeError(55)); // File already open
|
||||
}
|
||||
let p = pfad_normieren(pfad);
|
||||
if modus == Modus::Input && !p.exists() {
|
||||
return Err(RuntimeError(53)); // File not found
|
||||
}
|
||||
self.offen
|
||||
.insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
|
||||
Ok(())
|
||||
@@ -478,6 +596,7 @@ pub fn pfad_normieren(p: &str) -> PathBuf {
|
||||
fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
|
||||
use std::io::ErrorKind::*;
|
||||
match e.kind() {
|
||||
NotADirectory => RuntimeError(76), // Path not found
|
||||
NotFound => RuntimeError(53), // File not found
|
||||
PermissionDenied => RuntimeError(70), // Permission denied
|
||||
AlreadyExists => RuntimeError(58), // File already exists
|
||||
@@ -485,32 +604,39 @@ fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
|
||||
}
|
||||
}
|
||||
|
||||
/// NotFound am Dateieintrag (53) gegenüber fehlendem Elternpfad (76).
|
||||
fn fehler_am_pfad(e: std::io::Error, pfad: &Path) -> RuntimeError {
|
||||
if e.kind() == std::io::ErrorKind::NotFound
|
||||
&& pfad
|
||||
.parent()
|
||||
.is_some_and(|p| !p.as_os_str().is_empty() && !p.is_dir())
|
||||
{
|
||||
RuntimeError(76)
|
||||
} else {
|
||||
fehler_aus_io(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// `KILL` — Datei löschen.
|
||||
pub fn loeschen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
std::fs::remove_file(pfad_normieren(pfad)).map_err(fehler_aus_io)
|
||||
let p = pfad_normieren(pfad);
|
||||
std::fs::remove_file(&p).map_err(|e| fehler_am_pfad(e, &p))
|
||||
}
|
||||
|
||||
/// `NAME alt AS neu`.
|
||||
pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
|
||||
let (a, n) = (pfad_normieren(alt), pfad_normieren(neu));
|
||||
if !a.exists() {
|
||||
return Err(RuntimeError(53));
|
||||
}
|
||||
std::fs::metadata(&a).map_err(|e| fehler_am_pfad(e, &a))?;
|
||||
if n.exists() {
|
||||
return Err(RuntimeError(58)); // File already exists
|
||||
}
|
||||
std::fs::rename(a, n).map_err(fehler_aus_io)
|
||||
std::fs::rename(&a, &n).map_err(|e| fehler_am_pfad(e, &n))
|
||||
}
|
||||
|
||||
/// `MKDIR` / `RMDIR` / `CHDIR`.
|
||||
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
let p = pfad_normieren(pfad);
|
||||
if p.parent()
|
||||
.is_some_and(|e| !e.as_os_str().is_empty() && !e.exists())
|
||||
{
|
||||
return Err(RuntimeError(76)); // Path not found
|
||||
}
|
||||
std::fs::create_dir(p).map_err(fehler_aus_io)
|
||||
std::fs::create_dir(&p).map_err(|e| fehler_am_pfad(e, &p))
|
||||
}
|
||||
|
||||
pub fn verzeichnis_entfernen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
@@ -613,7 +739,7 @@ pub fn felder_trennen(zeile: &str) -> Vec<String> {
|
||||
/// Ein Element für `WRITE #` aufbereiten: Strings in Anführungszeichen.
|
||||
pub fn write_element(text: &str, ist_string: bool) -> String {
|
||||
if ist_string {
|
||||
format!("\"{text}\"")
|
||||
format!("\"{}\"", text.replace('"', "\"\""))
|
||||
} else {
|
||||
text.trim().to_string()
|
||||
}
|
||||
|
||||
@@ -1375,6 +1375,7 @@ fn builtin_id(b: Builtin) -> u16 {
|
||||
Builtin::DateSet => ids::DATE_SET,
|
||||
Builtin::TimeSet => ids::TIME_SET,
|
||||
Builtin::Now => ids::NOW,
|
||||
Builtin::TimezoneKnown => ids::TIMEZONEKNOWN,
|
||||
Builtin::DateSerial => ids::DATE_SERIAL,
|
||||
Builtin::TimeSerial => ids::TIME_SERIAL,
|
||||
Builtin::DateValue => ids::DATE_VALUE,
|
||||
|
||||
@@ -2570,8 +2570,9 @@ impl Vm {
|
||||
if let Some(taste) = self.rt.gepufferte_taste() {
|
||||
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
|
||||
if let Some(rest) = &mut eingabe.rest {
|
||||
let taste = self.rt.tastenanteil(taste, *rest);
|
||||
*rest -= taste.chars().count();
|
||||
eingabe.text.push_str(&taste);
|
||||
*rest -= 1;
|
||||
} else {
|
||||
match taste.as_str() {
|
||||
tb_runtime::host::taste::ENTER => {
|
||||
@@ -2872,19 +2873,26 @@ impl Vm {
|
||||
refs.reverse();
|
||||
let nummer = self.pop_i32()?;
|
||||
|
||||
let Some(zeile) = self.rt.dateien.get(nummer)?.zeile_lesen()? else {
|
||||
return Err(RuntimeError(62)); // Input past end of file
|
||||
};
|
||||
if line_mode {
|
||||
let zeile = self
|
||||
.rt
|
||||
.dateien
|
||||
.get(nummer)?
|
||||
.zeile_lesen()?
|
||||
.ok_or(RuntimeError(62))?;
|
||||
let r = refs.first().ok_or(RuntimeError(51))?;
|
||||
self.write_ref(r, Value::Str(Rc::from(zeile.as_str())))?;
|
||||
return Ok(());
|
||||
}
|
||||
let felder = tb_runtime::fileio::felder_trennen(&zeile);
|
||||
for (i, r) in refs.iter().enumerate() {
|
||||
let text = felder.get(i).map(String::as_str).unwrap_or("");
|
||||
for r in &refs {
|
||||
let text = self.rt.dateien.get(nummer)?.feld_lesen()?;
|
||||
let ziel = self.read_ref(r)?;
|
||||
let wert = input_value(&ziel, text).ok_or(RuntimeError(64))?; // Bad file name
|
||||
// Der Feldleser hat Quotes und äußeren Leerraum bereits ausgewertet.
|
||||
let wert = if matches!(ziel, Value::Str(_)) {
|
||||
Value::Str(Rc::from(text))
|
||||
} else {
|
||||
input_value(&ziel, &text).ok_or(RuntimeError(64))?
|
||||
};
|
||||
self.write_ref(r, wert)?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1516,3 +1516,203 @@ fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() {
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
|
||||
}
|
||||
|
||||
// ---- Laufzeit-Eingabe und Systemstatus ------------------------------------
|
||||
|
||||
#[test]
|
||||
fn input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend() {
|
||||
let tmp = TempVerzeichnis::neu("input_felder");
|
||||
let p = tmp.pfad("daten.txt");
|
||||
assert_eq!(out(&format!(
|
||||
"OPEN \"{p}\" FOR OUTPUT AS #1\nWRITE #1, \"a\", \"b\"\nWRITE #1, \"a,b\", 5\nWRITE #1, CHR$(34) + \"ä\" + CHR$(34), \"\"\nCLOSE #1\nOPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$\nINPUT #1, b$, c$, n%, d$, e$\nPRINT a$; b$; c$; n%; d$; LEN(e$); EOF(1)\nCLOSE"
|
||||
)), "aba,b 5 \"ä\" 0 -1 \n");
|
||||
for (data, expected) in [("\"a\"\n", 62), ("a", 62), ("", 62), ("\"a", 62)] {
|
||||
std::fs::write(&p, data).unwrap();
|
||||
assert_eq!(
|
||||
err_code(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$, b$")),
|
||||
expected
|
||||
);
|
||||
}
|
||||
std::fs::write(&p, "a,\r\n\r\n\"b\n,c\"\r\n").unwrap();
|
||||
assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$, b$, c$, d$\nPRINT a$; LEN(b$); LEN(c$); LEN(d$); EOF(1)")), "a 0 0 4 -1 \n");
|
||||
std::fs::write(&p, "a,").unwrap();
|
||||
assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$\nPRINT EOF(1);\nINPUT #1, b$\nPRINT LEN(b$); EOF(1)")), " 0 0 -1 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_datei_alle_lesearten_teilen_utf8_cursor_und_seek() {
|
||||
let tmp = TempVerzeichnis::neu("input_cursor");
|
||||
let p = tmp.pfad("daten.txt");
|
||||
std::fs::write(&p, "ä😀,rest\r\n\"a,b\",Ende\r\nZ").unwrap();
|
||||
assert_eq!(out(&format!(
|
||||
"OPEN \"{p}\" FOR INPUT AS #1\nPRINT INPUT$(1, #1); INPUT$(1, #1); SEEK(1)\nLINE INPUT #1, s$\nPRINT s$\nINPUT #1, s$\nPRINT s$; INPUT$(2, #1)\nLINE INPUT #1, s$\nPRINT s$\nPRINT INPUT$(1, #1); EOF(1); LEN(INPUT$(0, #1))\nSEEK #1, 1\nINPUT #1, s$\nPRINT s$; SEEK(1)\nCLOSE"
|
||||
)), "ä😀 7 \n,rest\na,bEn\nde\nZ-1 0 \nä😀 8 \n");
|
||||
for mode in ["INPUT", "BINARY"] {
|
||||
std::fs::write(&p, "ä😀x").unwrap();
|
||||
assert_eq!(
|
||||
out(&format!(
|
||||
"OPEN \"{p}\" FOR {mode} AS #1\nPRINT INPUT$(2, #1); INPUT$(1, #1); EOF(1)"
|
||||
)),
|
||||
"ä😀x-1 \n"
|
||||
);
|
||||
assert_eq!(
|
||||
err_code(&format!(
|
||||
"OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(4, #1)"
|
||||
)),
|
||||
62
|
||||
);
|
||||
for bad in [&[0xc3][..], &[0xff], &[0xe0, 0x80, 0x80], &[0xf0, 0x9f]] {
|
||||
std::fs::write(&p, bad).unwrap();
|
||||
assert_eq!(
|
||||
err_code(&format!(
|
||||
"OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(1, #1)"
|
||||
)),
|
||||
57
|
||||
);
|
||||
}
|
||||
}
|
||||
for input in ["INPUT #1, s$", "LINE INPUT #1, s$"] {
|
||||
assert_eq!(
|
||||
err_code(&format!("OPEN \"{p}\" FOR INPUT AS #1\n{input}")),
|
||||
57
|
||||
);
|
||||
}
|
||||
for mode in ["OUTPUT", "APPEND", "RANDOM"] {
|
||||
assert_eq!(
|
||||
err_code(&format!(
|
||||
"OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(1, #1)"
|
||||
)),
|
||||
54
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_s_teilt_sondertasten_und_bewahrt_rest_fuer_inkey() {
|
||||
use tb_runtime::host::Ereignis;
|
||||
for second in ["INPUT$(1)", "INKEY$"] {
|
||||
let src = format!("a$ = INPUT$(1)\nb$ = {second}\nc$ = INPUT$(2)\nPRINT LEN(a$); ASC(a$); LEN(b$); ASC(b$); c$; LEN(INKEY$)");
|
||||
let mut vm = Vm::new(tb_vm::compile_source("T", &src).unwrap());
|
||||
let mut host = CaptureHost::default();
|
||||
for text in ["\0;", "ä", "😀"] {
|
||||
host.ereignis(Ereignis::Taste(text.into(), 0));
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen),
|
||||
" 1 0 1 59 ä😀 0 \n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alle_mk_cv_paare_verlangen_exakte_bytezeichenbreite() {
|
||||
for (mk, cv, n, breite) in [
|
||||
("MKI$", "CVI", "-123", 2),
|
||||
("MKL$", "CVL", "123456", 4),
|
||||
("MKS$", "CVS", "1.5", 4),
|
||||
("MKD$", "CVD", "-12.25", 8),
|
||||
("MKC$", "CVC", "12.3456@", 8),
|
||||
] {
|
||||
assert_eq!(out(&format!("PRINT {cv}({mk}({n})) = {n}")), "-1 \n");
|
||||
for arg in [
|
||||
format!("LEFT$({mk}({n}), {})", breite - 1),
|
||||
format!("{mk}({n}) + CHR$(0)"),
|
||||
format!("STRING$({breite}, \"😀\")"),
|
||||
] {
|
||||
assert_eq!(err_code(&format!("PRINT {cv}({arg})")), 5, "{cv}({arg})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn datei_und_elternpfadfehler_sind_am_sprachzugang_unterscheidbar() {
|
||||
let tmp = TempVerzeichnis::neu("pfadfehler");
|
||||
let leaf = tmp.pfad("fehlt.txt");
|
||||
let parent = tmp.pfad("fehlt/datei.txt");
|
||||
let source = tmp.pfad("quelle.txt");
|
||||
std::fs::write(&source, "erhalten").unwrap();
|
||||
for (path, code) in [(&leaf, 53), (&parent, 76)] {
|
||||
for stmt in [
|
||||
format!("OPEN \"{path}\" FOR INPUT AS #1"),
|
||||
format!("KILL \"{path}\""),
|
||||
format!("NAME \"{path}\" AS \"{source}\""),
|
||||
] {
|
||||
assert_eq!(err_code(&stmt), code, "{stmt}");
|
||||
}
|
||||
}
|
||||
for stmt in [
|
||||
format!("MKDIR \"{parent}\""),
|
||||
format!("NAME \"{source}\" AS \"{parent}\""),
|
||||
format!("CHDIR \"{parent}\""),
|
||||
format!("RMDIR \"{parent}\""),
|
||||
format!("PRINT DIR$(\"{parent}\")"),
|
||||
] {
|
||||
assert_eq!(err_code(&stmt), 76, "{stmt}");
|
||||
}
|
||||
for mode in ["OUTPUT", "APPEND", "BINARY", "RANDOM"] {
|
||||
assert_eq!(err_code(&format!("OPEN \"{parent}\" FOR {mode} AS #1")), 76);
|
||||
}
|
||||
assert_eq!(std::fs::read_to_string(&source).unwrap(), "erhalten");
|
||||
let sub = tmp.pfad("unter");
|
||||
let a = format!("{sub}\\a.txt");
|
||||
let b = format!("{sub}/b.txt");
|
||||
assert_eq!(out(&format!("MKDIR \"{sub}\"\nOPEN \"{a}\" FOR OUTPUT AS #1\nWRITE #1, \"ok\"\nCLOSE\nNAME \"{a}\" AS \"{b}\"\nPRINT DIR$(\"{sub}/*.txt\"); LEN(DIR$(\"\"))\nOPEN \"{b}\" FOR INPUT AS #1\nINPUT #1, s$\nPRINT s$\nCLOSE\nKILL \"{b}\"\nRMDIR \"{sub}\"")), "b.txt 0 \nok\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezoneknown_hat_integer_signatur_und_zeigt_aktuellen_status() {
|
||||
for (zone, expected) in [(Zeitzone::Fest(0), "-1 \n"), (Zeitzone::Unbekannt, " 0 \n")] {
|
||||
assert_eq!(out_mit_zone("PRINT TIMEZONEKNOWN", zone), expected);
|
||||
}
|
||||
let diagnostics = tb_vm::compile_source("T", "PRINT TIMEZONEKNOWN(1)").unwrap_err();
|
||||
assert!(
|
||||
diagnostics
|
||||
.iter()
|
||||
.any(|d| d.message.contains("TIMEZONEKNOWN")),
|
||||
"{diagnostics:?}"
|
||||
);
|
||||
let mut vm = Vm::new(
|
||||
tb_vm::compile_source(
|
||||
"T",
|
||||
"x = TIMEZONEKNOWN\nSTOP\ny = TIMEZONEKNOWN\nSTOP\nz = TIMEZONEKNOWN\nEND",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
vm.rt.zonen_ermittler = |_| Some(7200);
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
assert!(matches!(vm.inspect("x"), Some(Value::Sng(-1.0))));
|
||||
vm.rt.zonen_ermittler = |_| None;
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
assert!(matches!(vm.inspect("y"), Some(Value::Sng(0.0))));
|
||||
vm.rt.zonen_ermittler = |_| Some(0);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("z"), Some(Value::Sng(-1.0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_puffergrenzen_und_binaere_records_bleiben_getrennt() {
|
||||
let tmp = TempVerzeichnis::neu("input_puffer");
|
||||
let p = tmp.pfad("daten.txt");
|
||||
for n in [8190, 8191, 8192] {
|
||||
std::fs::write(&p, format!("{}ä😀\r\nZ", "x".repeat(n))).unwrap();
|
||||
assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\na$ = INPUT$({n}, #1)\nPRINT INPUT$(2, #1)\nLINE INPUT #1, b$\nINPUT #1, c$\nPRINT LEN(b$); c$; EOF(1)")), "ä😀\n 0 Z-1 \n");
|
||||
}
|
||||
let mut bytes = "ä😀".as_bytes().to_vec();
|
||||
bytes.extend_from_slice(&513i16.to_le_bytes());
|
||||
bytes.push(b'Z');
|
||||
std::fs::write(&p, bytes).unwrap();
|
||||
assert_eq!(out(&format!("OPEN \"{p}\" FOR BINARY AS #1\nPRINT INPUT$(2, #1);\nGET #1, , n%\nPRINT n%; INPUT$(1, #1); EOF(1)\nSEEK #1, 7\nn% = 1027\nPUT #1, , n%\nSEEK #1, 7\nGET #1, , n%\nPRINT n%; INPUT$(1, #1)")), "ä😀 513 Z-1 \n 1027 Z\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sondertastenrest_wird_nicht_erneut_getrappt() {
|
||||
use tb_runtime::host::Ereignis;
|
||||
let src = "ON KEY(1) GOSUB H\nKEY(1) STOP\na$ = INPUT$(1)\nKEY(1) ON\nb$ = INPUT$(1)\nDOEVENTS\nPRINT ASC(a$); ASC(b$); n%\nEND\nH:\nn% = n% + 1\nRETURN";
|
||||
let mut vm = Vm::new(tb_vm::compile_source("T", src).unwrap());
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste("\0;".into(), 0));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 0 59 1 \n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user