From 5c0731f0b4e01b578150884fcd3897a8f09bf721 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 22 Oct 2025 10:03:43 +0200 Subject: [PATCH] separati gli scenari dalla configurazione aggiunta la sezione per lam creazione dei file temporanei sistemato il problema dell'invio dellle flag anche in tgtset --- README.md | 34 ++ scenarios.json | 1 + settings.json | 393 +---------------------- target_simulator/core/command_builder.py | 22 +- target_simulator/gui/sfp_debug_window.py | 25 +- target_simulator/utils/config_manager.py | 79 ++++- tests/core/test_command_builder.py | 23 ++ 7 files changed, 168 insertions(+), 409 deletions(-) create mode 100644 scenarios.json diff --git a/README.md b/README.md index 314fdeb..7981c93 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,37 @@ A brief description of target_simulator. ## License ... + + +## Note: debug GUI vs simulation command behaviour + +The project provides two different code paths for sending target update commands: + +- Debug GUI (SFP Packet Inspector / Simple Target Sender): + - When you press "Send Target" from the debug window, the GUI sends a + `tgtset` command that includes state qualifiers such as `/s` (active), + `/t` (traceable) and `/r` (restart) if the corresponding checkboxes are set. + - This behaviour is intentional: the debug UI is meant for manual testing and + for explicitly toggling target flags. + +- Runtime Simulation: + - The simulation engine and continuous updates use `tgtset` commands that + contain only the positional/kinematic parameters (range, azimuth, velocity, + heading, altitude) and do not include the state qualifiers. This avoids + unintentionally changing target lifecycle flags during normal simulation. + +Why this matters +- Historically the code used `tgtinit` in some debug flows. `tgtinit` sets + some different internal parameters (for example `heading_start`) and can + change target motion in unintended ways. To preserve correct simulation + behaviour, the debug window now sends `tgtset` + qualifiers while the + simulation keeps sending `tgtset` without qualifiers. + +Where to look in the code +- Debug GUI sender: `target_simulator/gui/sfp_debug_window.py` (`_on_send_target`). +- Command builders: `target_simulator/core/command_builder.py` (`build_tgtset_from_target_state`, `build_tgtset_selective`, `build_tgtinit`). +- Settings: debug file names and options are in `settings.json` (section `debug`) and `target_simulator/config.py` contains defaults. + +If you want the debug UI to exactly mimic runtime behaviour, uncheck the +Active/Traceable/Restart checkboxes before sending, or use the runtime APIs to +send `tgtset` without qualifiers. diff --git a/scenarios.json b/scenarios.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/scenarios.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/settings.json b/settings.json index 6ac6e14..cf70896 100644 --- a/settings.json +++ b/settings.json @@ -2,8 +2,8 @@ "general": { "scan_limit": 60, "max_range": 100, - "geometry": "1599x1024+453+144", - "last_selected_scenario": "scenario_9g", + "geometry": "1599x1024+626+57", + "last_selected_scenario": null, "connection": { "target": { "type": "sfp", @@ -39,389 +39,10 @@ } } }, - "scenarios": { - "scenario1": { - "name": "scenario1", - "targets": [ - { - "target_id": 0, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 10.0, - "target_altitude_ft": 10000.0, - "target_range_nm": 20.0, - "target_azimuth_deg": 0.0 - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 100.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 1670.9318999999994, - "target_heading_deg": 10.0 - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 400.0, - "target_altitude_ft": 10000.0, - "target_range_nm": 25.0, - "target_azimuth_deg": -20.0 - } - ], - "use_spline": false - } - ] - }, - "scenario2": { - "name": "scenario2", - "targets": [ - { - "target_id": 0, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 10.0, - "target_range_nm": 10.0, - "target_azimuth_deg": -3.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 0.0, - "target_heading_deg": 0.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 200.0, - "target_range_nm": 20.0, - "target_azimuth_deg": 10.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 200.0, - "target_range_nm": 30.0, - "target_azimuth_deg": -10.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 200.0, - "target_range_nm": 35.0, - "target_azimuth_deg": 10.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 200.0, - "target_range_nm": 35.0, - "target_azimuth_deg": 30.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 200.0, - "target_range_nm": 20.0, - "target_azimuth_deg": 45.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - } - ], - "use_spline": true - }, - { - "target_id": 1, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 10.0, - "target_range_nm": 10.0, - "target_azimuth_deg": 10.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 10.0, - "target_range_nm": 20.0, - "target_azimuth_deg": 20.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 30.0, - "target_range_nm": 30.0, - "target_azimuth_deg": 30.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 30.0, - "target_range_nm": 35.0, - "target_azimuth_deg": -10.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - } - ], - "use_spline": true - }, - { - "target_id": 2, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 1.0, - "target_range_nm": 28.0, - "target_azimuth_deg": 0.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 300.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Dynamic Maneuver", - "duration_s": 9.0, - "maneuver_speed_fps": 1519.0289999999995, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 9.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 100.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 1012.686, - "target_heading_deg": -90.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - } - ], - "use_spline": false - } - ] - }, - "scenario3": { - "name": "scenario3", - "targets": [ - { - "target_id": 0, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 10.0, - "target_range_nm": 5.0, - "target_azimuth_deg": 0.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 20.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 90.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 10.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 20.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 90.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 30.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": -180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - } - ], - "use_spline": true - } - ] - }, - "scenario_9g": { - "name": "scenario2", - "targets": [ - { - "target_id": 2, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 1.0, - "target_range_nm": 28.0, - "target_azimuth_deg": 0.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 300.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 180.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Dynamic Maneuver", - "duration_s": 9.0, - "maneuver_speed_fps": 1519.0289999999995, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 9.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly for Duration", - "duration_s": 100.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 1012.686, - "target_heading_deg": -90.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - } - ], - "use_spline": false - } - ] - }, - "scenario_dritto": { - "name": "scenario_dritto", - "targets": [ - { - "target_id": 0, - "active": true, - "traceable": true, - "trajectory": [ - { - "maneuver_type": "Fly to Point", - "duration_s": 1.0, - "target_range_nm": 20.0, - "target_azimuth_deg": -45.0, - "target_altitude_ft": 10000.0, - "target_velocity_fps": 506.343, - "target_heading_deg": 90.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - }, - { - "maneuver_type": "Fly to Point", - "duration_s": 100.0, - "target_range_nm": 20.0, - "target_azimuth_deg": 45.0, - "target_altitude_ft": 10000.0, - "longitudinal_acceleration_g": 0.0, - "lateral_acceleration_g": 0.0, - "vertical_acceleration_g": 0.0, - "turn_direction": "Right" - } - ], - "use_spline": false - } - ] - } + "debug": { + "enable_io_trace": true, + "temp_folder_name": "Temp", + "io_trace_sent_filename": "sent_positions.csv", + "io_trace_received_filename": "received_positions.csv" } } \ No newline at end of file diff --git a/target_simulator/core/command_builder.py b/target_simulator/core/command_builder.py index e65924f..7940ffd 100644 --- a/target_simulator/core/command_builder.py +++ b/target_simulator/core/command_builder.py @@ -37,7 +37,7 @@ def build_tgtinit(target: Target) -> str: return full_command -def build_tgtset_from_target_state(target: Target) -> str: +def build_tgtset_from_target_state(target: Target, include_flags: bool = False) -> str: """ Builds the 'tgtset' command from a target's CURRENT dynamic state. This is used for continuous updates during simulation. @@ -50,10 +50,21 @@ def build_tgtset_from_target_state(target: Target) -> str: f"{target.current_heading_deg:.2f}", f"{target.current_altitude_ft:.2f}", ] - # For live tgtset updates we only send the positional/kinematic parameters. - # Qualifiers such as /s or /t are part of initialization commands (tgtinit) - # and are not included in continuous update packets. + # For live tgtset updates we normally only send the positional/kinematic + # parameters. However, callers can request inclusion of qualifiers + # (/s, /-s, /t, /-t, /r) by setting include_flags=True (used by debug UI). command_parts = ["tgtset"] + [str(p) for p in params] + if include_flags: + if getattr(target, "active", False): + command_parts.append("/s") + else: + command_parts.append("/-s") + if getattr(target, "traceable", False): + command_parts.append("/t") + else: + command_parts.append("/-t") + if hasattr(target, "restart") and getattr(target, "restart", False): + command_parts.append("/r") full_command = " ".join(command_parts) logger.debug(f"Built command: {full_command!r}") @@ -79,6 +90,9 @@ def build_tgtset_selective(target_id: int, updates: Dict[str, Any]) -> str: qualifiers.append("/s" if updates["active"] else "/-s") if "traceable" in updates: qualifiers.append("/t" if updates["traceable"] else "/-t") + # Support restart qualifier for selective commands when requested + if "restart" in updates and updates.get("restart"): + qualifiers.append("/r") command_parts.extend(qualifiers) full_command = " ".join(command_parts) diff --git a/target_simulator/gui/sfp_debug_window.py b/target_simulator/gui/sfp_debug_window.py index 416831d..8880034 100644 --- a/target_simulator/gui/sfp_debug_window.py +++ b/target_simulator/gui/sfp_debug_window.py @@ -312,6 +312,14 @@ class SfpDebugWindow(tk.Toplevel): controls_frame, text="Send Target", command=self._on_send_target ) send_button.pack(side=tk.LEFT, padx=(10, 0)) + # Small helper label to clarify that this debug button will include + # state qualifiers (/s /t /r) in the sent tgtset command. + ttk.Label( + controls_frame, + text="(debug: sends /s /t /r if set)", + foreground="#555555", + font=("Segoe UI", 8), + ).pack(side=tk.LEFT, padx=(6, 0)) # --- Quick command buttons (moved here from connection frame) --- quick_cmd_frame = ttk.Frame(parent) @@ -524,7 +532,14 @@ class SfpDebugWindow(tk.Toplevel): self.notebook.add(self.json_tab, text="JSON") def _on_send_target(self): - """Callback to build and send a tgtinit command for a simple target.""" + """Callback to build and send a debug 'tgtset' command for a simple target. + + Notes: + - This debug sender uses `tgtset` with qualifiers (/s, /t, /r) when the + corresponding checkboxes are set in the UI. The runtime simulation + should continue to use `tgtset` without qualifiers for continuous + updates; qualifiers are only included here for manual debugging. + """ # 1. Collect data from UI ip = self.ip_var.get() @@ -595,8 +610,12 @@ class SfpDebugWindow(tk.Toplevel): temp_target.current_heading_deg = hdg_deg temp_target.current_altitude_ft = alt_ft - # 3. Build the command string - command_str = command_builder.build_tgtset_from_target_state(temp_target) + # 3. Build the command string: use tgtset but INCLUDE qualifiers + # so that the debug window sends the flags (/s, /t, /r) while the + # simulation runtime continues to use tgtset without flags. + command_str = command_builder.build_tgtset_from_target_state( + temp_target, include_flags=True + ) # Ensure the command is trimmed, prefixed with '$' and terminated with a newline command_str = command_str.strip() # if not command_str.startswith("$"): diff --git a/target_simulator/utils/config_manager.py b/target_simulator/utils/config_manager.py index 06cc1b8..49da573 100644 --- a/target_simulator/utils/config_manager.py +++ b/target_simulator/utils/config_manager.py @@ -39,7 +39,7 @@ class ConfigManager: """Handles reading and writing application settings and scenarios from a JSON file.""" - def __init__(self, filename: str = "settings.json"): + def __init__(self, filename: str = "settings.json", scenarios_filename: str = "scenarios.json"): """ Initializes the ConfigManager. @@ -55,21 +55,30 @@ class ConfigManager: ) self.filepath = os.path.join(application_path, filename) + self.scenarios_filepath = os.path.join(application_path, scenarios_filename) self._settings = self._load_or_initialize_settings() + # Load scenarios from separate file if present, otherwise keep any scenarios + # found inside settings.json (fallback). + self._scenarios = self._load_or_initialize_scenarios() def _load_or_initialize_settings(self) -> Dict[str, Any]: """Loads settings from the JSON file or initializes with a default structure.""" + # If someone calls this at runtime and scenarios were previously loaded + # from another location, clear them so the caller can decide how to + # reload scenarios (this supports tests that swap filepath at runtime). + if hasattr(self, "_scenarios"): + self._scenarios = {} + if not os.path.exists(self.filepath): return {"general": {}, "scenarios": {}} try: with open(self.filepath, "r", encoding="utf-8") as f: settings = json.load(f) - if ( - not isinstance(settings, dict) - or "general" not in settings - or "scenarios" not in settings - ): + if not isinstance(settings, dict) or "general" not in settings: + # If file contained only general settings (old style), wrap it return {"general": settings, "scenarios": {}} + # Keep scenarios key if present; actual source of truth for scenarios + # will be the separate scenarios file when available. return settings except (json.JSONDecodeError, IOError): return {"general": {}, "scenarios": {}} @@ -78,10 +87,50 @@ class ConfigManager: """Saves the current settings to the JSON file.""" try: with open(self.filepath, "w", encoding="utf-8") as f: - json.dump(self._settings, f, indent=4, cls=EnumEncoder) + # Write all settings except 'scenarios' (which is persisted + # separately). This preserves custom top-level keys while + # ensuring scenarios are stored in their dedicated file. + to_write = dict(self._settings) + if "scenarios" in to_write: + # don't duplicate scenarios in settings.json + del to_write["scenarios"] + json.dump(to_write, f, indent=4, cls=EnumEncoder) except IOError as e: print(f"Error saving settings to {self.filepath}: {e}") + def _load_or_initialize_scenarios(self) -> Dict[str, Any]: + """Loads scenarios from the separate scenarios file if present. + + Falls back to scenarios inside settings.json when scenarios.json is absent. + """ + # If scenarios file exists, load from it. + if os.path.exists(self.scenarios_filepath): + try: + with open(self.scenarios_filepath, "r", encoding="utf-8") as f: + scenarios = json.load(f) + if isinstance(scenarios, dict): + return scenarios + except (json.JSONDecodeError, IOError): + return {} + + # Fallback: try to read scenarios stored inside settings.json + try: + with open(self.filepath, "r", encoding="utf-8") as f: + settings = json.load(f) + if isinstance(settings, dict) and "scenarios" in settings: + return settings.get("scenarios", {}) + except Exception: + pass + return {} + + def _save_scenarios(self): + """Saves scenarios to the separate scenarios file.""" + try: + with open(self.scenarios_filepath, "w", encoding="utf-8") as f: + json.dump(self._scenarios, f, indent=4, cls=EnumEncoder) + except IOError as e: + print(f"Error saving scenarios to {self.scenarios_filepath}: {e}") + def get_general_settings(self) -> Dict[str, Any]: """Returns the general settings.""" return self._settings.get("general", {}) @@ -93,7 +142,7 @@ class ConfigManager: def get_scenario_names(self) -> List[str]: """Returns a list of all scenario names.""" - return list(self._settings.get("scenarios", {}).keys()) + return list(self._scenarios.keys()) def get_scenario(self, name: str) -> Optional[Dict[str, Any]]: """ @@ -105,7 +154,7 @@ class ConfigManager: Returns: A dictionary with the scenario data, or None if not found. """ - return self._settings.get("scenarios", {}).get(name) + return self._scenarios.get(name) def save_scenario(self, name: str, data: Dict[str, Any]): """ @@ -115,10 +164,8 @@ class ConfigManager: name: The name of the scenario to save. data: The dictionary of scenario data to save. """ - if "scenarios" not in self._settings: - self._settings["scenarios"] = {} - self._settings["scenarios"][name] = data - self._save_settings() + self._scenarios[name] = data + self._save_scenarios() def delete_scenario(self, name: str): """ @@ -127,6 +174,6 @@ class ConfigManager: Args: name: The name of the scenario to delete. """ - if "scenarios" in self._settings and name in self._settings["scenarios"]: - del self._settings["scenarios"][name] - self._save_settings() + if name in self._scenarios: + del self._scenarios[name] + self._save_scenarios() diff --git a/tests/core/test_command_builder.py b/tests/core/test_command_builder.py index e7d92e6..6e80647 100644 --- a/tests/core/test_command_builder.py +++ b/tests/core/test_command_builder.py @@ -68,3 +68,26 @@ def test_build_simple_commands(): assert command_builder.build_acunlatch() == "acunlatch" assert command_builder.build_tgtreset() == "tgtreset -1" assert command_builder.build_tgtreset(target_id=8) == "tgtreset 8" + + +def test_build_tgtset_with_flags(sample_target): + """Verifica che build_tgtset_from_target_state includa i qualificatori quando richiesto.""" + # Prepare a sample target with restart flag + sample_target.current_range_nm = 10.0 + sample_target.current_azimuth_deg = 0.0 + sample_target.current_velocity_fps = 300.0 + sample_target.current_heading_deg = 0.0 + sample_target.current_altitude_ft = 10000.0 + sample_target.active = True + sample_target.traceable = True + sample_target.restart = True + + cmd_with_flags = command_builder.build_tgtset_from_target_state( + sample_target, include_flags=True + ) + # Order of qualifiers should be /s /t /r + assert cmd_with_flags.endswith("/s /t /r") + + # Without flags it should not contain qualifiers + cmd_no_flags = command_builder.build_tgtset_from_target_state(sample_target) + assert not cmd_no_flags.endswith("/s /t /r")