mirror of
https://github.com/QIDITECH/moonraker.git
synced 2026-01-30 16:18:44 +03:00
QIDI moonraker
This commit is contained in:
403
tests/assets/klipper/base_printer.cfg
Normal file
403
tests/assets/klipper/base_printer.cfg
Normal file
@@ -0,0 +1,403 @@
|
||||
[mcu]
|
||||
serial: /dev/serial/by-id/usb
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 1500
|
||||
max_z_velocity: 15
|
||||
max_z_accel: 200
|
||||
|
||||
[stepper_x]
|
||||
microsteps: 16
|
||||
step_pin: PC0
|
||||
dir_pin: !PL0
|
||||
enable_pin: !PA7
|
||||
rotation_distance: 32
|
||||
endstop_pin: tmc2130_stepper_x:virtual_endstop
|
||||
position_endstop: 0
|
||||
position_min: 0
|
||||
position_max: 250
|
||||
homing_speed: 50
|
||||
homing_retract_dist: 0
|
||||
|
||||
[tmc2130 stepper_x]
|
||||
cs_pin: PG0
|
||||
interpolate: True
|
||||
run_current: .281738
|
||||
hold_current: .281738
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK2
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 2
|
||||
driver_PWM_AMPL: 230
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[stepper_y]
|
||||
microsteps: 16
|
||||
step_pin: PC1
|
||||
dir_pin: PL1
|
||||
enable_pin: !PA6
|
||||
rotation_distance: 32
|
||||
endstop_pin: tmc2130_stepper_y:virtual_endstop
|
||||
position_endstop: -4
|
||||
position_max: 210
|
||||
position_min: -4
|
||||
homing_speed: 50
|
||||
homing_retract_dist: 0
|
||||
|
||||
[tmc2130 stepper_y]
|
||||
cs_pin: PG2
|
||||
interpolate: True
|
||||
run_current: .3480291
|
||||
hold_current: .3480291
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK7
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 2
|
||||
driver_PWM_AMPL: 235
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[stepper_z]
|
||||
microsteps: 16
|
||||
step_pin: PC2
|
||||
dir_pin: !PL2
|
||||
enable_pin: !PA5
|
||||
rotation_distance: 8
|
||||
endstop_pin: probe:z_virtual_endstop
|
||||
position_max: 220
|
||||
position_min: -2
|
||||
homing_speed: 13.333
|
||||
|
||||
[tmc2130 stepper_z]
|
||||
cs_pin: PK5
|
||||
interpolate: True
|
||||
run_current: .53033
|
||||
hold_current: .53033
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK6
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 4
|
||||
driver_PWM_AMPL: 200
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 4
|
||||
|
||||
[extruder]
|
||||
microsteps: 8
|
||||
step_pin: PC3
|
||||
dir_pin: PL6
|
||||
enable_pin: !PA4
|
||||
rotation_distance: 6.53061216
|
||||
full_steps_per_rotation: 400
|
||||
nozzle_diameter: 0.4
|
||||
filament_diameter: 1.750
|
||||
max_extrude_cross_section: 50.0
|
||||
# Allows to load filament and purge up to 500mm
|
||||
max_extrude_only_distance: 500.0
|
||||
max_extrude_only_velocity: 120.0
|
||||
max_extrude_only_accel: 1250.0
|
||||
heater_pin: PE5
|
||||
sensor_type: ATC Semitec 104GT-2
|
||||
sensor_pin: PF0
|
||||
control: pid
|
||||
pid_Kp: 16.13
|
||||
pid_Ki: 1.1625
|
||||
pid_Kd: 56.23
|
||||
min_temp: 0
|
||||
max_temp: 305
|
||||
|
||||
[tmc2130 extruder]
|
||||
cs_pin: PK4
|
||||
interpolate: True
|
||||
run_current: 0.41432
|
||||
hold_current: 0.3
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK3
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD:4
|
||||
driver_PWM_AMPL: 240
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PG5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PF2
|
||||
control: pid
|
||||
pid_Kp: 126.13
|
||||
pid_Ki: 4.3
|
||||
pid_Kd: 924.76
|
||||
min_temp: 0
|
||||
max_temp: 125
|
||||
|
||||
[verify_heater heater_bed]
|
||||
max_error: 240
|
||||
check_gain_time: 120
|
||||
|
||||
[heater_fan nozzle_cooling_fan]
|
||||
pin: PH5
|
||||
heater: extruder
|
||||
heater_temp: 50.0
|
||||
|
||||
[fan]
|
||||
pin: PH3
|
||||
|
||||
[display]
|
||||
lcd_type: hd44780
|
||||
rs_pin: PD5
|
||||
e_pin: PF7
|
||||
d4_pin: PF5
|
||||
d5_pin: PG4
|
||||
d6_pin: PH7
|
||||
d7_pin: PG3
|
||||
encoder_pins: ^PJ1,^PJ2
|
||||
click_pin: ^!PH6
|
||||
|
||||
[pause_resume]
|
||||
|
||||
[virtual_sdcard]
|
||||
path: ${gcode_path}
|
||||
|
||||
[respond]
|
||||
default_type: command
|
||||
|
||||
[probe]
|
||||
pin: PB4
|
||||
x_offset: 23
|
||||
y_offset: 5
|
||||
z_offset: 0.8
|
||||
speed: 12.0
|
||||
|
||||
[bed_mesh]
|
||||
speed: 140
|
||||
horizontal_move_z: 2
|
||||
mesh_min: 24, 6
|
||||
mesh_max: 238, 210
|
||||
probe_count: 7
|
||||
mesh_pps: 2
|
||||
fade_start: 1
|
||||
fade_end: 10
|
||||
fade_target: 0
|
||||
move_check_distance: 15
|
||||
algorithm: bicubic
|
||||
bicubic_tension: .2
|
||||
relative_reference_index: 24
|
||||
faulty_region_1_min: 116.75, 41.81
|
||||
faulty_region_1_max: 133.25, 78.81
|
||||
faulty_region_2_min: 156.5, 99.31
|
||||
faulty_region_2_max: 193.5, 115.81
|
||||
faulty_region_3_min: 116.75, 136.21
|
||||
faulty_region_3_max: 133.25, 173.31
|
||||
|
||||
[homing_override]
|
||||
gcode:
|
||||
G1 Z3 F600
|
||||
G28 X0 Y0
|
||||
G1 X131 Y108 F5000
|
||||
G28 Z0
|
||||
axes: Z
|
||||
set_position_x: 0
|
||||
set_position_y: 0
|
||||
set_position_z: 0
|
||||
|
||||
[output_pin BEEPER_pin]
|
||||
pin: PH2
|
||||
pwm: True
|
||||
value: 0
|
||||
shutdown_value:0
|
||||
cycle_time: 0.001
|
||||
scale: 1000
|
||||
|
||||
[force_move]
|
||||
enable_force_move: True
|
||||
|
||||
[idle_timeout]
|
||||
gcode:
|
||||
M104 S0
|
||||
M84
|
||||
|
||||
[gcode_macro PAUSE]
|
||||
rename_existing: BASE_PAUSE
|
||||
gcode:
|
||||
{% if not printer.pause_resume.is_paused %}
|
||||
M600
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro M600]
|
||||
variable_extr_temp: 0
|
||||
gcode:
|
||||
{% set X = params.X|default(100) %}
|
||||
{% set Y = params.Y|default(100) %}
|
||||
{% set Z = params.Z|default(100) %}
|
||||
BASE_PAUSE
|
||||
SET_GCODE_VARIABLE MACRO=M600 VARIABLE=extr_temp VALUE={printer.extruder.target}
|
||||
G91
|
||||
{% if printer.extruder.temperature|float > 180 %}
|
||||
G1 E-.8 F2700
|
||||
{% endif %}
|
||||
G1 Z{Z}
|
||||
G90
|
||||
G1 X{X} Y{Y} F3000
|
||||
|
||||
[gcode_macro RESUME]
|
||||
rename_existing: BASE_RESUME
|
||||
gcode:
|
||||
{% if printer.pause_resume.is_paused %}
|
||||
{% if printer["gcode_macro M600"].extr_temp %}
|
||||
M109 S{printer["gcode_macro M600"].extr_temp}
|
||||
{% endif %}
|
||||
BASE_RESUME
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro LOAD_FILAMENT]
|
||||
gcode:
|
||||
M117 Loading Filament...
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 E50 F400
|
||||
G1 E25 F100
|
||||
G90
|
||||
G92 E0.0
|
||||
M400
|
||||
M117 Load Complete
|
||||
UPDATE_DELAYED_GCODE ID=clear_display DURATION=5
|
||||
|
||||
[gcode_macro UNLOAD_FILAMENT]
|
||||
gcode:
|
||||
M117 Unloading Filament...
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 E-32 F5200
|
||||
G1 E-10 F100
|
||||
G1 E-38 F1000
|
||||
G90
|
||||
G92 E0.0
|
||||
M400
|
||||
M300 S300 P1000
|
||||
M117 Remove Filament Now!
|
||||
UPDATE_DELAYED_GCODE ID=clear_display DURATION=5
|
||||
|
||||
[gcode_macro G80]
|
||||
gcode:
|
||||
G28
|
||||
BED_MESH_CALIBRATE
|
||||
G1 X0 Y0 F4000
|
||||
|
||||
[gcode_macro G81]
|
||||
gcode:
|
||||
{% set S = params.S|default(0) %}
|
||||
BED_MESH_OUTPUT CENTER_ZERO={S}
|
||||
|
||||
[gcode_macro M300]
|
||||
gcode:
|
||||
{% set S = params.S|default(1000) %}
|
||||
{% set P = params.P|default(100) %}
|
||||
SET_PIN PIN=BEEPER_pin VALUE={S}
|
||||
G4 P{P}
|
||||
SET_PIN PIN=BEEPER_pin VALUE=0
|
||||
|
||||
[gcode_macro PRINT_START]
|
||||
gcode:
|
||||
{% set MATERIAL = params.MATERIAL|default("Unknown") %}
|
||||
{% set LAYER_HEIGHT = params.LAYER_HEIGHT|default(0) %}
|
||||
M83
|
||||
CLEAR_PAUSE
|
||||
SET_IDLE_TIMEOUT TIMEOUT=600
|
||||
SET_PRESSURE_ADVANCE ADVANCE=0
|
||||
SET_GCODE_OFFSET Z=0
|
||||
G90
|
||||
M104 S170
|
||||
M190 S{params.BTMP}
|
||||
M109 S170
|
||||
G80
|
||||
M104 S{params.ETMP}
|
||||
G1 X1 Y-3.0 Z20 F1000.0 ; go outside print area
|
||||
M109 S{params.ETMP}
|
||||
G1 Z.4
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 X60.0 E9.0 F1000.0 ; intro line
|
||||
G1 X40.0 E12.5 F1000.0 ; intro line
|
||||
G90
|
||||
G92 E0.0
|
||||
{% if MATERIAL != "PLA" %}
|
||||
SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=1
|
||||
{% endif %}
|
||||
{% if LAYER_HEIGHT|float < 0.051 %}
|
||||
M221 S100
|
||||
{% else %}
|
||||
M221 S95
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro PRINT_END]
|
||||
gcode:
|
||||
CLEAR_PAUSE
|
||||
M400
|
||||
BED_MESH_CLEAR
|
||||
G92 E0.0
|
||||
G91
|
||||
{% if printer.gcode_move.gcode_position.x > 20 %}
|
||||
{% if printer.gcode_move.gcode_position.y > 20 %}
|
||||
G1 Z+1.00 X-20.0 Y-20.0 F20000 ;short quick move to disengage from print
|
||||
{% else %}
|
||||
G1 Z+1.00 X-20.0 F20000 ;short quick move to disengage from print
|
||||
{% endif %}
|
||||
{% elif printer.gcode_move.gcode_position.y > 20 %}
|
||||
G1 Z+1.00 Y-20.0 F20000 ;short quick move to disengage from print
|
||||
{% endif %}
|
||||
G1 E-8.00 F500 ;retract additional filament to prevent oozing
|
||||
G90
|
||||
{% if printer.gcode_move.gcode_position.z < 100 %}
|
||||
G0 Z100 F1500
|
||||
{% elif printer.gcode_move.gcode_position.z < 190 %}
|
||||
G91
|
||||
G0 Z10 F1500
|
||||
G90
|
||||
{% endif %}
|
||||
G0 X10 Y200 F6000
|
||||
SET_GCODE_OFFSET Z=0 MOVE=1
|
||||
TURN_OFF_HEATERS
|
||||
SET_VELOCITY_LIMIT VELOCITY=300 SQUARE_CORNER_VELOCITY=5
|
||||
M84
|
||||
M107
|
||||
M204 S3000
|
||||
M221 S100
|
||||
|
||||
[gcode_macro CANCEL_PRINT]
|
||||
rename_existing: BASE_CANCEL_PRINT
|
||||
gcode:
|
||||
PAUSE
|
||||
SDCARD_RESET_FILE
|
||||
PRINT_END
|
||||
CLEAR_PAUSE
|
||||
|
||||
[gcode_macro TEST_REMOTE_METHOD]
|
||||
gcode:
|
||||
{action_call_remote_method(method="moonraker_test",
|
||||
result="test")}
|
||||
403
tests/assets/klipper/error_printer.cfg
Normal file
403
tests/assets/klipper/error_printer.cfg
Normal file
@@ -0,0 +1,403 @@
|
||||
[mcu]
|
||||
serial: /dev/serial/by-id/usb
|
||||
|
||||
printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 1500
|
||||
max_z_velocity: 15
|
||||
max_z_accel: 200
|
||||
|
||||
[stepper_x]
|
||||
microsteps: 16
|
||||
step_pin: PC0
|
||||
dir_pin: !PL0
|
||||
enable_pin: !PA7
|
||||
rotation_distance: 32
|
||||
endstop_pin: tmc2130_stepper_x:virtual_endstop
|
||||
position_endstop: 0
|
||||
position_min: 0
|
||||
position_max: 250
|
||||
homing_speed: 50
|
||||
homing_retract_dist: 0
|
||||
|
||||
[tmc2130 stepper_x]
|
||||
cs_pin: PG0
|
||||
interpolate: True
|
||||
run_current: .281738
|
||||
hold_current: .281738
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK2
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 2
|
||||
driver_PWM_AMPL: 230
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[stepper_y]
|
||||
microsteps: 16
|
||||
step_pin: PC1
|
||||
dir_pin: PL1
|
||||
enable_pin: !PA6
|
||||
rotation_distance: 32
|
||||
endstop_pin: tmc2130_stepper_y:virtual_endstop
|
||||
position_endstop: -4
|
||||
position_max: 210
|
||||
position_min: -4
|
||||
homing_speed: 50
|
||||
homing_retract_dist: 0
|
||||
|
||||
[tmc2130 stepper_y]
|
||||
cs_pin: PG2
|
||||
interpolate: True
|
||||
run_current: .3480291
|
||||
hold_current: .3480291
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK7
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 2
|
||||
driver_PWM_AMPL: 235
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[stepper_z]
|
||||
microsteps: 16
|
||||
step_pin: PC2
|
||||
dir_pin: !PL2
|
||||
enable_pin: !PA5
|
||||
rotation_distance: 8
|
||||
endstop_pin: probe:z_virtual_endstop
|
||||
position_max: 220
|
||||
position_min: -2
|
||||
homing_speed: 13.333
|
||||
|
||||
[tmc2130 stepper_z]
|
||||
cs_pin: PK5
|
||||
interpolate: True
|
||||
run_current: .53033
|
||||
hold_current: .53033
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK6
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 4
|
||||
driver_PWM_AMPL: 200
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 4
|
||||
|
||||
[extruder]
|
||||
microsteps: 8
|
||||
step_pin: PC3
|
||||
dir_pin: PL6
|
||||
enable_pin: !PA4
|
||||
rotation_distance: 6.53061216
|
||||
full_steps_per_rotation: 400
|
||||
nozzle_diameter: 0.4
|
||||
filament_diameter: 1.750
|
||||
max_extrude_cross_section: 50.0
|
||||
# Allows to load filament and purge up to 500mm
|
||||
max_extrude_only_distance: 500.0
|
||||
max_extrude_only_velocity: 120.0
|
||||
max_extrude_only_accel: 1250.0
|
||||
heater_pin: PE5
|
||||
sensor_type: ATC Semitec 104GT-2
|
||||
sensor_pin: PF0
|
||||
control: pid
|
||||
pid_Kp: 16.13
|
||||
pid_Ki: 1.1625
|
||||
pid_Kd: 56.23
|
||||
min_temp: 0
|
||||
max_temp: 305
|
||||
|
||||
[tmc2130 extruder]
|
||||
cs_pin: PK4
|
||||
interpolate: True
|
||||
run_current: 0.41432
|
||||
hold_current: 0.3
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK3
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD:4
|
||||
driver_PWM_AMPL: 240
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PG5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PF2
|
||||
control: pid
|
||||
pid_Kp: 126.13
|
||||
pid_Ki: 4.3
|
||||
pid_Kd: 924.76
|
||||
min_temp: 0
|
||||
max_temp: 125
|
||||
|
||||
[verify_heater heater_bed]
|
||||
max_error: 240
|
||||
check_gain_time: 120
|
||||
|
||||
[heater_fan nozzle_cooling_fan]
|
||||
pin: PH5
|
||||
heater: extruder
|
||||
heater_temp: 50.0
|
||||
|
||||
[fan]
|
||||
pin: PH3
|
||||
|
||||
[display]
|
||||
lcd_type: hd44780
|
||||
rs_pin: PD5
|
||||
e_pin: PF7
|
||||
d4_pin: PF5
|
||||
d5_pin: PG4
|
||||
d6_pin: PH7
|
||||
d7_pin: PG3
|
||||
encoder_pins: ^PJ1,^PJ2
|
||||
click_pin: ^!PH6
|
||||
|
||||
[pause_resume]
|
||||
|
||||
[virtual_sdcard]
|
||||
path: ${gcode_path}
|
||||
|
||||
[respond]
|
||||
default_type: command
|
||||
|
||||
[probe]
|
||||
pin: PB4
|
||||
x_offset: 23
|
||||
y_offset: 5
|
||||
z_offset: 0.8
|
||||
speed: 12.0
|
||||
|
||||
[bed_mesh]
|
||||
speed: 140
|
||||
horizontal_move_z: 2
|
||||
mesh_min: 24, 6
|
||||
mesh_max: 238, 210
|
||||
probe_count: 7
|
||||
mesh_pps: 2
|
||||
fade_start: 1
|
||||
fade_end: 10
|
||||
fade_target: 0
|
||||
move_check_distance: 15
|
||||
algorithm: bicubic
|
||||
bicubic_tension: .2
|
||||
relative_reference_index: 24
|
||||
faulty_region_1_min: 116.75, 41.81
|
||||
faulty_region_1_max: 133.25, 78.81
|
||||
faulty_region_2_min: 156.5, 99.31
|
||||
faulty_region_2_max: 193.5, 115.81
|
||||
faulty_region_3_min: 116.75, 136.21
|
||||
faulty_region_3_max: 133.25, 173.31
|
||||
|
||||
[homing_override]
|
||||
gcode:
|
||||
G1 Z3 F600
|
||||
G28 X0 Y0
|
||||
G1 X131 Y108 F5000
|
||||
G28 Z0
|
||||
axes: Z
|
||||
set_position_x: 0
|
||||
set_position_y: 0
|
||||
set_position_z: 0
|
||||
|
||||
[output_pin BEEPER_pin]
|
||||
pin: PH2
|
||||
pwm: True
|
||||
value: 0
|
||||
shutdown_value:0
|
||||
cycle_time: 0.001
|
||||
scale: 1000
|
||||
|
||||
[force_move]
|
||||
enable_force_move: True
|
||||
|
||||
[idle_timeout]
|
||||
gcode:
|
||||
M104 S0
|
||||
M84
|
||||
|
||||
[gcode_macro PAUSE]
|
||||
rename_existing: BASE_PAUSE
|
||||
gcode:
|
||||
{% if not printer.pause_resume.is_paused %}
|
||||
M600
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro M600]
|
||||
variable_extr_temp: 0
|
||||
gcode:
|
||||
{% set X = params.X|default(100) %}
|
||||
{% set Y = params.Y|default(100) %}
|
||||
{% set Z = params.Z|default(100) %}
|
||||
BASE_PAUSE
|
||||
SET_GCODE_VARIABLE MACRO=M600 VARIABLE=extr_temp VALUE={printer.extruder.target}
|
||||
G91
|
||||
{% if printer.extruder.temperature|float > 180 %}
|
||||
G1 E-.8 F2700
|
||||
{% endif %}
|
||||
G1 Z{Z}
|
||||
G90
|
||||
G1 X{X} Y{Y} F3000
|
||||
|
||||
[gcode_macro RESUME]
|
||||
rename_existing: BASE_RESUME
|
||||
gcode:
|
||||
{% if printer.pause_resume.is_paused %}
|
||||
{% if printer["gcode_macro M600"].extr_temp %}
|
||||
M109 S{printer["gcode_macro M600"].extr_temp}
|
||||
{% endif %}
|
||||
BASE_RESUME
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro LOAD_FILAMENT]
|
||||
gcode:
|
||||
M117 Loading Filament...
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 E50 F400
|
||||
G1 E25 F100
|
||||
G90
|
||||
G92 E0.0
|
||||
M400
|
||||
M117 Load Complete
|
||||
UPDATE_DELAYED_GCODE ID=clear_display DURATION=5
|
||||
|
||||
[gcode_macro UNLOAD_FILAMENT]
|
||||
gcode:
|
||||
M117 Unloading Filament...
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 E-32 F5200
|
||||
G1 E-10 F100
|
||||
G1 E-38 F1000
|
||||
G90
|
||||
G92 E0.0
|
||||
M400
|
||||
M300 S300 P1000
|
||||
M117 Remove Filament Now!
|
||||
UPDATE_DELAYED_GCODE ID=clear_display DURATION=5
|
||||
|
||||
[gcode_macro G80]
|
||||
gcode:
|
||||
G28
|
||||
BED_MESH_CALIBRATE
|
||||
G1 X0 Y0 F4000
|
||||
|
||||
[gcode_macro G81]
|
||||
gcode:
|
||||
{% set S = params.S|default(0) %}
|
||||
BED_MESH_OUTPUT CENTER_ZERO={S}
|
||||
|
||||
[gcode_macro M300]
|
||||
gcode:
|
||||
{% set S = params.S|default(1000) %}
|
||||
{% set P = params.P|default(100) %}
|
||||
SET_PIN PIN=BEEPER_pin VALUE={S}
|
||||
G4 P{P}
|
||||
SET_PIN PIN=BEEPER_pin VALUE=0
|
||||
|
||||
[gcode_macro PRINT_START]
|
||||
gcode:
|
||||
{% set MATERIAL = params.MATERIAL|default("Unknown") %}
|
||||
{% set LAYER_HEIGHT = params.LAYER_HEIGHT|default(0) %}
|
||||
M83
|
||||
CLEAR_PAUSE
|
||||
SET_IDLE_TIMEOUT TIMEOUT=600
|
||||
SET_PRESSURE_ADVANCE ADVANCE=0
|
||||
SET_GCODE_OFFSET Z=0
|
||||
G90
|
||||
M104 S170
|
||||
M190 S{params.BTMP}
|
||||
M109 S170
|
||||
G80
|
||||
M104 S{params.ETMP}
|
||||
G1 X1 Y-3.0 Z20 F1000.0 ; go outside print area
|
||||
M109 S{params.ETMP}
|
||||
G1 Z.4
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 X60.0 E9.0 F1000.0 ; intro line
|
||||
G1 X40.0 E12.5 F1000.0 ; intro line
|
||||
G90
|
||||
G92 E0.0
|
||||
{% if MATERIAL != "PLA" %}
|
||||
SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=1
|
||||
{% endif %}
|
||||
{% if LAYER_HEIGHT|float < 0.051 %}
|
||||
M221 S100
|
||||
{% else %}
|
||||
M221 S95
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro PRINT_END]
|
||||
gcode:
|
||||
CLEAR_PAUSE
|
||||
M400
|
||||
BED_MESH_CLEAR
|
||||
G92 E0.0
|
||||
G91
|
||||
{% if printer.gcode_move.gcode_position.x > 20 %}
|
||||
{% if printer.gcode_move.gcode_position.y > 20 %}
|
||||
G1 Z+1.00 X-20.0 Y-20.0 F20000 ;short quick move to disengage from print
|
||||
{% else %}
|
||||
G1 Z+1.00 X-20.0 F20000 ;short quick move to disengage from print
|
||||
{% endif %}
|
||||
{% elif printer.gcode_move.gcode_position.y > 20 %}
|
||||
G1 Z+1.00 Y-20.0 F20000 ;short quick move to disengage from print
|
||||
{% endif %}
|
||||
G1 E-8.00 F500 ;retract additional filament to prevent oozing
|
||||
G90
|
||||
{% if printer.gcode_move.gcode_position.z < 100 %}
|
||||
G0 Z100 F1500
|
||||
{% elif printer.gcode_move.gcode_position.z < 190 %}
|
||||
G91
|
||||
G0 Z10 F1500
|
||||
G90
|
||||
{% endif %}
|
||||
G0 X10 Y200 F6000
|
||||
SET_GCODE_OFFSET Z=0 MOVE=1
|
||||
TURN_OFF_HEATERS
|
||||
SET_VELOCITY_LIMIT VELOCITY=300 SQUARE_CORNER_VELOCITY=5
|
||||
M84
|
||||
M107
|
||||
M204 S3000
|
||||
M221 S100
|
||||
|
||||
[gcode_macro CANCEL_PRINT]
|
||||
rename_existing: BASE_CANCEL_PRINT
|
||||
gcode:
|
||||
PAUSE
|
||||
SDCARD_RESET_FILE
|
||||
PRINT_END
|
||||
CLEAR_PAUSE
|
||||
|
||||
[gcode_macro TEST_REMOTE_METHOD]
|
||||
gcode:
|
||||
{action_call_remote_method(method="moonraker_test",
|
||||
result="test")}
|
||||
1
tests/assets/klipper/klipper.dict
Normal file
1
tests/assets/klipper/klipper.dict
Normal file
File diff suppressed because one or more lines are too long
347
tests/assets/klipper/missing_reqs.cfg
Normal file
347
tests/assets/klipper/missing_reqs.cfg
Normal file
@@ -0,0 +1,347 @@
|
||||
[mcu]
|
||||
serial: /dev/serial/by-id/usb
|
||||
|
||||
[printer]
|
||||
kinematics: cartesian
|
||||
max_velocity: 300
|
||||
max_accel: 1500
|
||||
max_z_velocity: 15
|
||||
max_z_accel: 200
|
||||
|
||||
[stepper_x]
|
||||
microsteps: 16
|
||||
step_pin: PC0
|
||||
dir_pin: !PL0
|
||||
enable_pin: !PA7
|
||||
rotation_distance: 32
|
||||
endstop_pin: tmc2130_stepper_x:virtual_endstop
|
||||
position_endstop: 0
|
||||
position_min: 0
|
||||
position_max: 250
|
||||
homing_speed: 50
|
||||
homing_retract_dist: 0
|
||||
|
||||
[tmc2130 stepper_x]
|
||||
cs_pin: PG0
|
||||
interpolate: True
|
||||
run_current: .281738
|
||||
hold_current: .281738
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK2
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 2
|
||||
driver_PWM_AMPL: 230
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[stepper_y]
|
||||
microsteps: 16
|
||||
step_pin: PC1
|
||||
dir_pin: PL1
|
||||
enable_pin: !PA6
|
||||
rotation_distance: 32
|
||||
endstop_pin: tmc2130_stepper_y:virtual_endstop
|
||||
position_endstop: -4
|
||||
position_max: 210
|
||||
position_min: -4
|
||||
homing_speed: 50
|
||||
homing_retract_dist: 0
|
||||
|
||||
[tmc2130 stepper_y]
|
||||
cs_pin: PG2
|
||||
interpolate: True
|
||||
run_current: .3480291
|
||||
hold_current: .3480291
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK7
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 2
|
||||
driver_PWM_AMPL: 235
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[stepper_z]
|
||||
microsteps: 16
|
||||
step_pin: PC2
|
||||
dir_pin: !PL2
|
||||
enable_pin: !PA5
|
||||
rotation_distance: 8
|
||||
endstop_pin: probe:z_virtual_endstop
|
||||
position_max: 220
|
||||
position_min: -2
|
||||
homing_speed: 13.333
|
||||
|
||||
[tmc2130 stepper_z]
|
||||
cs_pin: PK5
|
||||
interpolate: True
|
||||
run_current: .53033
|
||||
hold_current: .53033
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK6
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD: 4
|
||||
driver_PWM_AMPL: 200
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 4
|
||||
|
||||
[extruder]
|
||||
microsteps: 8
|
||||
step_pin: PC3
|
||||
dir_pin: PL6
|
||||
enable_pin: !PA4
|
||||
rotation_distance: 6.53061216
|
||||
full_steps_per_rotation: 400
|
||||
nozzle_diameter: 0.4
|
||||
filament_diameter: 1.750
|
||||
max_extrude_cross_section: 50.0
|
||||
# Allows to load filament and purge up to 500mm
|
||||
max_extrude_only_distance: 500.0
|
||||
max_extrude_only_velocity: 120.0
|
||||
max_extrude_only_accel: 1250.0
|
||||
heater_pin: PE5
|
||||
sensor_type: ATC Semitec 104GT-2
|
||||
sensor_pin: PF0
|
||||
control: pid
|
||||
pid_Kp: 16.13
|
||||
pid_Ki: 1.1625
|
||||
pid_Kd: 56.23
|
||||
min_temp: 0
|
||||
max_temp: 305
|
||||
|
||||
[tmc2130 extruder]
|
||||
cs_pin: PK4
|
||||
interpolate: True
|
||||
run_current: 0.41432
|
||||
hold_current: 0.3
|
||||
sense_resistor: 0.220
|
||||
diag1_pin: !PK3
|
||||
driver_IHOLDDELAY: 8
|
||||
driver_TPOWERDOWN: 0
|
||||
driver_TBL: 2
|
||||
driver_TOFF: 3
|
||||
driver_HEND: 1
|
||||
driver_HSTRT: 5
|
||||
driver_PWM_FREQ: 2
|
||||
driver_PWM_GRAD:4
|
||||
driver_PWM_AMPL: 240
|
||||
driver_PWM_AUTOSCALE: True
|
||||
driver_SGT: 3
|
||||
|
||||
[heater_bed]
|
||||
heater_pin: PG5
|
||||
sensor_type: EPCOS 100K B57560G104F
|
||||
sensor_pin: PF2
|
||||
control: pid
|
||||
pid_Kp: 126.13
|
||||
pid_Ki: 4.3
|
||||
pid_Kd: 924.76
|
||||
min_temp: 0
|
||||
max_temp: 125
|
||||
|
||||
[verify_heater heater_bed]
|
||||
max_error: 240
|
||||
check_gain_time: 120
|
||||
|
||||
[heater_fan nozzle_cooling_fan]
|
||||
pin: PH5
|
||||
heater: extruder
|
||||
heater_temp: 50.0
|
||||
|
||||
[fan]
|
||||
pin: PH3
|
||||
|
||||
[respond]
|
||||
default_type: command
|
||||
|
||||
[probe]
|
||||
pin: PB4
|
||||
x_offset: 23
|
||||
y_offset: 5
|
||||
z_offset: 0.8
|
||||
speed: 12.0
|
||||
|
||||
[bed_mesh]
|
||||
speed: 140
|
||||
horizontal_move_z: 2
|
||||
mesh_min: 24, 6
|
||||
mesh_max: 238, 210
|
||||
probe_count: 7
|
||||
mesh_pps: 2
|
||||
fade_start: 1
|
||||
fade_end: 10
|
||||
fade_target: 0
|
||||
move_check_distance: 15
|
||||
algorithm: bicubic
|
||||
bicubic_tension: .2
|
||||
relative_reference_index: 24
|
||||
faulty_region_1_min: 116.75, 41.81
|
||||
faulty_region_1_max: 133.25, 78.81
|
||||
faulty_region_2_min: 156.5, 99.31
|
||||
faulty_region_2_max: 193.5, 115.81
|
||||
faulty_region_3_min: 116.75, 136.21
|
||||
faulty_region_3_max: 133.25, 173.31
|
||||
|
||||
[homing_override]
|
||||
gcode:
|
||||
G1 Z3 F600
|
||||
G28 X0 Y0
|
||||
G1 X131 Y108 F5000
|
||||
G28 Z0
|
||||
axes: Z
|
||||
set_position_x: 0
|
||||
set_position_y: 0
|
||||
set_position_z: 0
|
||||
|
||||
[output_pin BEEPER_pin]
|
||||
pin: PH2
|
||||
pwm: True
|
||||
value: 0
|
||||
shutdown_value:0
|
||||
cycle_time: 0.001
|
||||
scale: 1000
|
||||
|
||||
[force_move]
|
||||
enable_force_move: True
|
||||
|
||||
[idle_timeout]
|
||||
gcode:
|
||||
M104 S0
|
||||
M84
|
||||
|
||||
[gcode_macro LOAD_FILAMENT]
|
||||
gcode:
|
||||
M117 Loading Filament...
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 E50 F400
|
||||
G1 E25 F100
|
||||
G90
|
||||
G92 E0.0
|
||||
M400
|
||||
M117 Load Complete
|
||||
UPDATE_DELAYED_GCODE ID=clear_display DURATION=5
|
||||
|
||||
[gcode_macro UNLOAD_FILAMENT]
|
||||
gcode:
|
||||
M117 Unloading Filament...
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 E-32 F5200
|
||||
G1 E-10 F100
|
||||
G1 E-38 F1000
|
||||
G90
|
||||
G92 E0.0
|
||||
M400
|
||||
M300 S300 P1000
|
||||
M117 Remove Filament Now!
|
||||
UPDATE_DELAYED_GCODE ID=clear_display DURATION=5
|
||||
|
||||
[gcode_macro G80]
|
||||
gcode:
|
||||
G28
|
||||
BED_MESH_CALIBRATE
|
||||
G1 X0 Y0 F4000
|
||||
|
||||
[gcode_macro G81]
|
||||
gcode:
|
||||
{% set S = params.S|default(0) %}
|
||||
BED_MESH_OUTPUT CENTER_ZERO={S}
|
||||
|
||||
[gcode_macro M300]
|
||||
gcode:
|
||||
{% set S = params.S|default(1000) %}
|
||||
{% set P = params.P|default(100) %}
|
||||
SET_PIN PIN=BEEPER_pin VALUE={S}
|
||||
G4 P{P}
|
||||
SET_PIN PIN=BEEPER_pin VALUE=0
|
||||
|
||||
[gcode_macro PRINT_START]
|
||||
gcode:
|
||||
{% set MATERIAL = params.MATERIAL|default("Unknown") %}
|
||||
{% set LAYER_HEIGHT = params.LAYER_HEIGHT|default(0) %}
|
||||
M83
|
||||
CLEAR_PAUSE
|
||||
SET_IDLE_TIMEOUT TIMEOUT=600
|
||||
SET_PRESSURE_ADVANCE ADVANCE=0
|
||||
SET_GCODE_OFFSET Z=0
|
||||
G90
|
||||
M104 S170
|
||||
M190 S{params.BTMP}
|
||||
M109 S170
|
||||
G80
|
||||
M104 S{params.ETMP}
|
||||
G1 X1 Y-3.0 Z20 F1000.0 ; go outside print area
|
||||
M109 S{params.ETMP}
|
||||
G1 Z.4
|
||||
G92 E0.0
|
||||
G91
|
||||
G1 X60.0 E9.0 F1000.0 ; intro line
|
||||
G1 X40.0 E12.5 F1000.0 ; intro line
|
||||
G90
|
||||
G92 E0.0
|
||||
{% if MATERIAL != "PLA" %}
|
||||
SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=1
|
||||
{% endif %}
|
||||
{% if LAYER_HEIGHT|float < 0.051 %}
|
||||
M221 S100
|
||||
{% else %}
|
||||
M221 S95
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro PRINT_END]
|
||||
gcode:
|
||||
CLEAR_PAUSE
|
||||
M400
|
||||
BED_MESH_CLEAR
|
||||
G92 E0.0
|
||||
G91
|
||||
{% if printer.gcode_move.gcode_position.x > 20 %}
|
||||
{% if printer.gcode_move.gcode_position.y > 20 %}
|
||||
G1 Z+1.00 X-20.0 Y-20.0 F20000 ;short quick move to disengage from print
|
||||
{% else %}
|
||||
G1 Z+1.00 X-20.0 F20000 ;short quick move to disengage from print
|
||||
{% endif %}
|
||||
{% elif printer.gcode_move.gcode_position.y > 20 %}
|
||||
G1 Z+1.00 Y-20.0 F20000 ;short quick move to disengage from print
|
||||
{% endif %}
|
||||
G1 E-8.00 F500 ;retract additional filament to prevent oozing
|
||||
G90
|
||||
{% if printer.gcode_move.gcode_position.z < 100 %}
|
||||
G0 Z100 F1500
|
||||
{% elif printer.gcode_move.gcode_position.z < 190 %}
|
||||
G91
|
||||
G0 Z10 F1500
|
||||
G90
|
||||
{% endif %}
|
||||
G0 X10 Y200 F6000
|
||||
SET_GCODE_OFFSET Z=0 MOVE=1
|
||||
TURN_OFF_HEATERS
|
||||
SET_VELOCITY_LIMIT VELOCITY=300 SQUARE_CORNER_VELOCITY=5
|
||||
M84
|
||||
M107
|
||||
M204 S3000
|
||||
M221 S100
|
||||
|
||||
|
||||
[gcode_macro TEST_REMOTE_METHOD]
|
||||
gcode:
|
||||
{action_call_remote_method(method="moonraker_test",
|
||||
result="test")}
|
||||
16
tests/assets/moonraker/bare_db.cdb
Normal file
16
tests/assets/moonraker/bare_db.cdb
Normal file
@@ -0,0 +1,16 @@
|
||||
+32,24:TU9PTlJBS0VSX0RBVEFCQVNFX1NUQVJU->bmFtZXNwYWNlX2NvdW50PTU=
|
||||
+36,12:bmFtZXNwYWNlX2F1dGhvcml6ZWRfdXNlcnM=->ZW50cmllcz0x
|
||||
+20,148:X0FQSV9LRVlfVVNFUl8=->eyJ1c2VybmFtZSI6ICJfQVBJX0tFWV9VU0VSXyIsICJhcGlfa2V5IjogIjg4ZTdlMjA0MDU3YjQzYTdiNTI3ZGEwZDQzNjQ1MDg5IiwgImNyZWF0ZWRfb24iOiAxNjQ1NDkwOTExLjM5NzI1OTd9
|
||||
+32,12:bmFtZXNwYWNlX2djb2RlX21ldGFkYXRh->ZW50cmllcz0w
|
||||
+24,12:bmFtZXNwYWNlX2hpc3Rvcnk=->ZW50cmllcz0w
|
||||
+28,12:bmFtZXNwYWNlX21vb25yYWtlcg==->ZW50cmllcz0z
|
||||
+12,236:ZGF0YWJhc2U=->eyJkZWJ1Z19jb3VudGVyIjogMiwgInVuc2FmZV9zaHV0ZG93bnMiOiAxLCAicHJvdGVjdGVkX25hbWVzcGFjZXMiOiBbImdjb2RlX21ldGFkYXRhIiwgImhpc3RvcnkiLCAibW9vbnJha2VyIiwgInVwZGF0ZV9tYW5hZ2VyIl0sICJmb3JiaWRkZW5fbmFtZXNwYWNlcyI6IFsiYXV0aG9yaXplZF91c2VycyJdfQ==
|
||||
+24,12:ZGF0YWJhc2VfdmVyc2lvbg==->cQEAAAAAAAAA
|
||||
+16,84:ZmlsZV9tYW5hZ2Vy->eyJtZXRhZGF0YV92ZXJzaW9uIjogMywgImdjb2RlX3BhdGgiOiAiL2hvbWUvcGkvZ2NvZGVfZmlsZXMifQ==
|
||||
+32,12:bmFtZXNwYWNlX3VwZGF0ZV9tYW5hZ2Vy->ZW50cmllcz02
|
||||
+8,400:Zmx1aWRk->eyJsYXN0X2NvbmZpZ19oYXNoIjogImIyNDE4OTgyZmVhOTg1ZmZlN2ZlODFhOWQ4MWI0MDUwMThmMDFhYjM5MTNmNTk4MmJhMzllZjY4NzFiZjE3NDkiLCAibGFzdF9yZWZyZXNoX3RpbWUiOiAxNjQ1NDkwOTI2LjkwOTYzOTEsICJ2ZXJzaW9uIjogInYxLjE2LjIiLCAicmVtb3RlX3ZlcnNpb24iOiAidjEuMTYuMiIsICJkbF9pbmZvIjogWyJodHRwczovL2dpdGh1Yi5jb20vZmx1aWRkLWNvcmUvZmx1aWRkL3JlbGVhc2VzL2Rvd25sb2FkL3YxLjE2LjIvZmx1aWRkLnppcCIsICJhcHBsaWNhdGlvbi96aXAiLCA5NTE4NjU1XX0=
|
||||
+12,3072:a2xpcHBlcg==->eyJsYXN0X2NvbmZpZ19oYXNoIjogIjg4OTMzZjgyNTVhMTQyNDI2YjM1ODdhYTY0MDdlNTZmNDllZDlmZWM2MWZhOTViMTVmY2Q2NmQ1ZDE3MGU5MGEiLCAibGFzdF9yZWZyZXNoX3RpbWUiOiAxNjQ1NDkwOTIyLjQxNTY0OTIsICJpc192YWxpZCI6IHRydWUsICJuZWVkX2NoYW5uZWxfdXBkYXRlIjogZmFsc2UsICJyZXBvX3ZhbGlkIjogdHJ1ZSwgImdpdF9vd25lciI6ICJLbGlwcGVyM2QiLCAiZ2l0X3JlcG9fbmFtZSI6ICJrbGlwcGVyIiwgImdpdF9yZW1vdGUiOiAib3JpZ2luIiwgImdpdF9icmFuY2giOiAibWFzdGVyIiwgImN1cnJlbnRfdmVyc2lvbiI6ICJ2MC4xMC4wLTI3MSIsICJ1cHN0cmVhbV92ZXJzaW9uIjogInYwLjEwLjAtMjc2IiwgImN1cnJlbnRfY29tbWl0IjogIjhiMGM2ZmNiMDg5NzY5ZjcwZWNiYjExY2MzNzkzZGNkNjFmNDQ1ZGQiLCAidXBzdHJlYW1fY29tbWl0IjogIjJiMmNhYThmMDUwZDMyZWZlMTY1OWU4ZDdjNzQzMWQwN2U5ZTY3YTAiLCAidXBzdHJlYW1fdXJsIjogImh0dHBzOi8vZ2l0aHViLmNvbS9LbGlwcGVyM2Qva2xpcHBlci5naXQiLCAiZnVsbF92ZXJzaW9uX3N0cmluZyI6ICJ2MC4xMC4wLTI3MS1nOGIwYzZmY2ItZGlydHkiLCAiYnJhbmNoZXMiOiBbImRldi13ZWJob29rcy0yMDIxMTExNCIsICJkZXYtd2ViaG9va3MtZml4IiwgIm1hc3RlciJdLCAiZGlydHkiOiB0cnVlLCAiaGVhZF9kZXRhY2hlZCI6IGZhbHNlLCAiZ2l0X21lc3NhZ2VzIjogW10sICJjb21taXRzX2JlaGluZCI6IFt7InNoYSI6ICIyYjJjYWE4ZjA1MGQzMmVmZTE2NTllOGQ3Yzc0MzFkMDdlOWU2N2EwIiwgImF1dGhvciI6ICJGcmFuayBUYWNraXR0IiwgImRhdGUiOiAiMTY0NTQ2Nzk3OCIsICJzdWJqZWN0IjogImtsaXBweS1yZXF1aXJlbWVudHM6IFBpbiBtYXJrdXBzYWZlPT0xLjEuMSB0byBmaXggcHl0aG9uMyAoIzUyODYpIiwgIm1lc3NhZ2UiOiAiTWFya3Vwc2FmZSB1cGRhdGVkIGFuZCB0aGUgbGF0ZXN0IHZlcnNpb24gbm8gbG9uZ2VyIGluY2x1ZGVzIGBzb2Z0X3VuaWNvZGVgXHJcblxyXG5TaWduZWQtb2ZmLWJ5OiBGcmFua2x5biBUYWNraXR0IDxnaXRAZnJhbmsuYWY+IiwgInRhZyI6IG51bGx9LCB7InNoYSI6ICI5ZTE1MzIxNDE4OWQxMDdmZWRjMTJiODNhZWZkYzQyZWZkOTE5NmY5IiwgImF1dGhvciI6ICJLZXZpbiBPJ0Nvbm5vciIsICJkYXRlIjogIjE2NDU0NjQwMjEiLCAic3ViamVjdCI6ICJkb2NzOiBNaW5vciB3b3JkaW5nIGNoYW5nZSB0byBFeGFtcGxlX0NvbmZpZ3MubWQiLCAibWVzc2FnZSI6ICJTaWduZWQtb2ZmLWJ5OiBLZXZpbiBPJ0Nvbm5vciA8a2V2aW5Aa29jb25ub3IubmV0PiIsICJ0YWciOiBudWxsfSwgeyJzaGEiOiAiNzIwMmE1ZGE4ZTIzZGJkZTI4YTE3Y2I2MDNlZWUzMDM4NWZkZDk1MSIsICJhdXRob3IiOiAiS2V2aW4gTydDb25ub3IiLCAiZGF0ZSI6ICIxNjQ1NDYzODUwIiwgInN1YmplY3QiOiAiZG9jczogTWlub3Igd29yZGluZyBjaGFuZ2UgaW4gRXhhbXBsZV9Db25maWdzLm1kIiwgIm1lc3NhZ2UiOiAiU2lnbmVkLW9mZi1ieTogS2V2aW4gTydDb25ub3IgPGtldmluQGtvY29ubm9yLm5ldD4iLCAidGFnIjogbnVsbH0sIHsic2hhIjogIjc0ZGJkOGE4ZTQxYmM5ZDJiMDk3Yzk5N2Y0ZTk3NWI2MGVmZTY4MTEiLCAiYXV0aG9yIjogIktldmluIE8nQ29ubm9yIiwgImRhdGUiOiAiMTY0NTQ2MzY5OSIsICJzdWJqZWN0IjogImRvY3M6IEZpeCBFeGFtcGxlX0NvbmZpZ3MubWQgbGlzdCByZW5kZXJpbmciLCAibWVzc2FnZSI6ICJNa2RvY3MgZG9lc24ndCBzdXBwb3J0IGEgdGhpcmQgbGV2ZWwgb2YgbGlzdCBuZXN0aW5nLlxuXG5TaWduZWQtb2ZmLWJ5OiBLZXZpbiBPJ0Nvbm5vciA8a2V2aW5Aa29jb25ub3IubmV0PiIsICJ0YWciOiBudWxsfSwgeyJzaGEiOiAiYzNiYWE2NzFhNWY0YjZkNjk5YjdiY2FiNTdkZmEwNWJhZWMwYmNlMCIsICJhdXRob3IiOiAiS2V2aW4gTydDb25ub3IiLCAiZGF0ZSI6ICIxNjQ1NDYzMDg1IiwgInN1YmplY3QiOiAiZG9jczogVXBkYXRlIEV4YW1wbGVfQ29uZmlncy5tZCIsICJtZXNzYWdlIjogIkRvY3VtZW50IHRoYXQgc3BhY2VzIGFuZCBzcGVjaWFsIGNoYXJhY3RlcnMgc2hvdWxkIG5vdCBiZSBpbiB0aGVcbmNvbmZpZyBmaWxlbmFtZS5cblxuUmVtb3ZlIHJlZmVyZW5jZSB0byBzdGVwX2Rpc3RhbmNlIGFuZCBwaW5fbWFwIGRlcHJlY2F0ZWQgZmVhdHVyZXMsIGFzXG50aG9zZSBmZWF0dXJlcyBhcmUgbm93IGZ1bGx5IHJlbW92ZWQuXG5cblNpZ25lZC1vZmYtYnk6IEtldmluIE8nQ29ubm9yIDxrZXZpbkBrb2Nvbm5vci5uZXQ+IiwgInRhZyI6IG51bGx9XX0=
|
||||
+12,404:bWFpbnNhaWw=->eyJsYXN0X2NvbmZpZ19oYXNoIjogIjFlNDRlOWZkZDQ2YmI1MzYxN2IwZjJkNjg1YmNhODBkM2MzMzUxYTA3YzA5YmM2NzQyMDA0NWFjNTQxMzAyZjQiLCAibGFzdF9yZWZyZXNoX3RpbWUiOiAxNjQ1NDkwOTI2LjQ1ODIzOTMsICJ2ZXJzaW9uIjogInYyLjAuMSIsICJyZW1vdGVfdmVyc2lvbiI6ICJ2Mi4xLjIiLCAiZGxfaW5mbyI6IFsiaHR0cHM6Ly9naXRodWIuY29tL21haW5zYWlsLWNyZXcvbWFpbnNhaWwvcmVsZWFzZXMvZG93bmxvYWQvdjIuMS4yL21haW5zYWlsLnppcCIsICJhcHBsaWNhdGlvbi96aXAiLCAzNTEyNzYxXX0=
|
||||
+12,1636:bW9vbnJha2Vy->eyJsYXN0X2NvbmZpZ19oYXNoIjogImVjNDEwMWQ4MWIzYzc5MzgzYjIyN2MwOGQwYTg4NDk5Mjg5NDM1ZmFlMGI0MTc5N2U2MWU1NjdjZWEyM2MyNjkiLCAibGFzdF9yZWZyZXNoX3RpbWUiOiAxNjQ1NDkwOTI0LjI4MzY1NTYsICJpc192YWxpZCI6IHRydWUsICJuZWVkX2NoYW5uZWxfdXBkYXRlIjogZmFsc2UsICJyZXBvX3ZhbGlkIjogdHJ1ZSwgImdpdF9vd25lciI6ICI/IiwgImdpdF9yZXBvX25hbWUiOiAibW9vbnJha2VyIiwgImdpdF9yZW1vdGUiOiAiYXJrc2luZSIsICJnaXRfYnJhbmNoIjogImRldi1kYXRhYmFzZS1hc3luYy0yNTAxMjAyMiIsICJjdXJyZW50X3ZlcnNpb24iOiAidjAuNy4xLTQxOCIsICJ1cHN0cmVhbV92ZXJzaW9uIjogInYwLjcuMS00MTUiLCAiY3VycmVudF9jb21taXQiOiAiODRiOGZkNDZmOWEzNjVhYmI5ZWIzY2NiNThjZjdlOWFhZGRmNjJmMiIsICJ1cHN0cmVhbV9jb21taXQiOiAiODA2OTA1MmRmYmU3OTY2ZjljNWY2ZjkwMWU3ZmIyMWFjMmFkMjJjOSIsICJ1cHN0cmVhbV91cmwiOiAiZ2l0Oi8vZXJpYy13b3JrLmhvbWUvbW9vbnJha2VyIiwgImZ1bGxfdmVyc2lvbl9zdHJpbmciOiAidjAuNy4xLTQxOC1nODRiOGZkNCIsICJicmFuY2hlcyI6IFsiZGV2LWNwdV90aHJvdHRsZWRfcGVyZi0yMDIxMTEwMyIsICJkZXYtdXBkYXRlLW1hbmFnZXItbXVsdGljbGllbnQiLCAibWFzdGVyIl0sICJkaXJ0eSI6IGZhbHNlLCAiaGVhZF9kZXRhY2hlZCI6IHRydWUsICJnaXRfbWVzc2FnZXMiOiBbXSwgImNvbW1pdHNfYmVoaW5kIjogW3sic2hhIjogIjgwNjkwNTJkZmJlNzk2NmY5YzVmNmY5MDFlN2ZiMjFhYzJhZDIyYzkiLCAiYXV0aG9yIjogIkVyaWMgQ2FsbGFoYW4iLCAiZGF0ZSI6ICIxNjQ1NDgyNDA5IiwgInN1YmplY3QiOiAic2NyaXB0czogaW50cm9kdWNlIGRidG9vbCIsICJtZXNzYWdlIjogIlRoaXMgdG9vbCBtYXkgYmUgdXNlZCB0byBiYWNrdXAgYW5kIHJlc3RvcmUgTW9vbnJha2VyJ3MgbG1kYlxuZGF0YWJhc2Ugd2l0aG91dCBkZXBlbmRpbmcgb24gdGhlIFwibG1kYi11dGlsc1wiIHBhY2thZ2UuICBUaGVcbmJhY2t1cCBpcyBkb25lIHRvIGEgcGxhaW4gdGV4dCBmaWxlIGluIGNkYiBmb3JtYXQsIHNvIGEgYmFja3VwXG5tYXkgYmUgcmVzdG9yZWQgb24gYW55IHBsYXRmb3JtLlxuXG5TaWduZWQtb2ZmLWJ5OiAgRXJpYyBDYWxsYWhhbiA8YXJrc2luZS5jb2RlQGdtYWlsLmNvbT4iLCAidGFnIjogbnVsbH1dfQ==
|
||||
+12,928:bW9vbnRlc3Q=->eyJsYXN0X2NvbmZpZ19oYXNoIjogIjgwYzY5NjgwNWU3MTczOWIyNWEzYTFiNjNhMTc1YmM5Y2Q1NGVkM2U5YTBiMzBhNDhhNzAzYWFkZWI2YjNmNmMiLCAibGFzdF9yZWZyZXNoX3RpbWUiOiAxNjQ1NDkwOTI3LjgzMDQ0NzQsICJpc192YWxpZCI6IHRydWUsICJuZWVkX2NoYW5uZWxfdXBkYXRlIjogZmFsc2UsICJyZXBvX3ZhbGlkIjogdHJ1ZSwgImdpdF9vd25lciI6ICJhcmtzaW5lIiwgImdpdF9yZXBvX25hbWUiOiAibW9vbnRlc3QiLCAiZ2l0X3JlbW90ZSI6ICJvcmlnaW4iLCAiZ2l0X2JyYW5jaCI6ICJtYXN0ZXIiLCAiY3VycmVudF92ZXJzaW9uIjogInYwLjAuMS0yIiwgInVwc3RyZWFtX3ZlcnNpb24iOiAidjAuMC4xLTIiLCAiY3VycmVudF9jb21taXQiOiAiNWI0Yjk0ODBkYmQxODZiMTY2ZDM2NTVjMTFiNGY2NDBkYzEzNTA5YiIsICJ1cHN0cmVhbV9jb21taXQiOiAiNWI0Yjk0ODBkYmQxODZiMTY2ZDM2NTVjMTFiNGY2NDBkYzEzNTA5YiIsICJ1cHN0cmVhbV91cmwiOiAiaHR0cHM6Ly9naXRodWIuY29tL2Fya3NpbmUvbW9vbnRlc3QuZ2l0IiwgImZ1bGxfdmVyc2lvbl9zdHJpbmciOiAidjAuMC4xLTItZzViNGI5NDgiLCAiYnJhbmNoZXMiOiBbIm1hc3RlciJdLCAiZGlydHkiOiBmYWxzZSwgImhlYWRfZGV0YWNoZWQiOiBmYWxzZSwgImdpdF9tZXNzYWdlcyI6IFtdLCAiY29tbWl0c19iZWhpbmQiOiBbXX0=
|
||||
+8,108:c3lzdGVt->eyJsYXN0X2NvbmZpZ19oYXNoIjogIiIsICJsYXN0X3JlZnJlc2hfdGltZSI6IDE2NDU0OTA5MTQuMTU3MzQwOCwgInBhY2thZ2VzIjogW119
|
||||
18
tests/assets/moonraker/base_server.conf
Normal file
18
tests/assets/moonraker/base_server.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
[server]
|
||||
host: 0.0.0.0
|
||||
port: 7010
|
||||
ssl_port: 7011
|
||||
klippy_uds_address: ${klippy_uds_path}
|
||||
|
||||
[database]
|
||||
database_path: ${database_path}
|
||||
|
||||
[machine]
|
||||
provider: none
|
||||
|
||||
[file_manager]
|
||||
config_path: ${config_path}
|
||||
log_path: ${log_path}
|
||||
|
||||
[secrets]
|
||||
secrets_path: ${secrets_path}
|
||||
20
tests/assets/moonraker/base_server_ssl.conf
Normal file
20
tests/assets/moonraker/base_server_ssl.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
[server]
|
||||
host: 0.0.0.0
|
||||
port: 7010
|
||||
ssl_port: 7011
|
||||
ssl_certificate_path: ${ssl_certificate_path}
|
||||
ssl_key_path: ${ssl_key_path}
|
||||
klippy_uds_address: ${klippy_uds_path}
|
||||
|
||||
[database]
|
||||
database_path: ${database_path}
|
||||
|
||||
[machine]
|
||||
provider: none
|
||||
|
||||
[file_manager]
|
||||
config_path: ${config_path}
|
||||
log_path: ${log_path}
|
||||
|
||||
[secrets]
|
||||
secrets_path: ${secrets_path}
|
||||
18
tests/assets/moonraker/invalid_config.conf
Normal file
18
tests/assets/moonraker/invalid_config.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
[server]
|
||||
host: 0.0.0.0
|
||||
port: 7010
|
||||
klippy_uds_address: ${klippy_uds_path}
|
||||
|
||||
# Syntax error
|
||||
database]
|
||||
database_path: ${database_path}
|
||||
|
||||
[machine]
|
||||
provider: none
|
||||
|
||||
[file_manager]
|
||||
config_path: ${config_path}
|
||||
log_path: ${log_path}
|
||||
|
||||
[secrets]
|
||||
secrets_path: ${secrets_path}
|
||||
3
tests/assets/moonraker/secrets.ini
Normal file
3
tests/assets/moonraker/secrets.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[mqtt_credentials]
|
||||
username: mqttuser
|
||||
password: mqttpass
|
||||
6
tests/assets/moonraker/secrets.json
Normal file
6
tests/assets/moonraker/secrets.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"mqtt_credentials": {
|
||||
"username": "mqttuser",
|
||||
"password": "mqttpass"
|
||||
}
|
||||
}
|
||||
39
tests/assets/moonraker/supplemental.conf
Normal file
39
tests/assets/moonraker/supplemental.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
[prefix_sec one]
|
||||
|
||||
[prefix_sec two]
|
||||
|
||||
[prefix_sec three]
|
||||
|
||||
[test_options]
|
||||
test_int: 1
|
||||
test_float: 3.5
|
||||
test_bool: True
|
||||
test_string: Hello World
|
||||
test_list:
|
||||
one
|
||||
two
|
||||
three
|
||||
test_int_list: 1,2,3
|
||||
test_float_list: 1.5,2.8,3.2
|
||||
test_multi_list:
|
||||
1,2,3
|
||||
4,5,6
|
||||
test_dict:
|
||||
one=1
|
||||
two=2
|
||||
three=3
|
||||
test_dict_empty_field:
|
||||
one=test
|
||||
two
|
||||
three
|
||||
test_template: {secrets.mqtt_credentials.username}
|
||||
test_gpio: gpiochip0/gpio26
|
||||
test_gpio_no_chip: gpio26
|
||||
test_gpio_invert: !gpiochip0/gpio26
|
||||
test_gpio_no_chip_invert: !gpio26
|
||||
# The following four options should result in an error, cant
|
||||
# pullup/pulldown an output pin
|
||||
test_gpio_pullup: ^gpiochip0/gpio26
|
||||
test_gpio_pullup_no_chip: ^gpio26
|
||||
test_gpio_pulldown: ~gpiochip0/gpio26
|
||||
test_gpio_pulldown_no_chip: ~gpio26
|
||||
22
tests/assets/moonraker/unparsed_server.conf
Normal file
22
tests/assets/moonraker/unparsed_server.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
[server]
|
||||
host: 0.0.0.0
|
||||
port: 7010
|
||||
klippy_uds_address: ${klippy_uds_path}
|
||||
# Add an option that is not registered, should
|
||||
# generate a warning
|
||||
unknown_option: True
|
||||
|
||||
[machine]
|
||||
provider: none
|
||||
|
||||
[database]
|
||||
database_path: ${database_path}
|
||||
|
||||
[file_manager]
|
||||
config_path: ${config_path}
|
||||
log_path: ${log_path}
|
||||
|
||||
[secrets]
|
||||
secrets_path: ${secrets_path}
|
||||
|
||||
[machine unparsed]
|
||||
244
tests/conftest.py
Normal file
244
tests/conftest.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import asyncio
|
||||
import shutil
|
||||
import re
|
||||
import pathlib
|
||||
import sys
|
||||
import shlex
|
||||
import tempfile
|
||||
import subprocess
|
||||
from typing import Iterator, Dict, AsyncIterator, Any
|
||||
from moonraker import Server
|
||||
from eventloop import EventLoop
|
||||
import utils
|
||||
import dbtool
|
||||
from fixtures import KlippyProcess, HttpClient, WebsocketClient
|
||||
|
||||
ASSETS = pathlib.Path(__file__).parent.joinpath("assets")
|
||||
|
||||
need_klippy_restart = pytest.StashKey[bool]()
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser, pluginmanager):
|
||||
parser.addoption("--klipper-path", action="store", dest="klipper_path")
|
||||
parser.addoption("--klipper-exec", action="store", dest="klipper_exec")
|
||||
|
||||
def interpolate_config(source_path: pathlib.Path,
|
||||
dest_path: pathlib.Path,
|
||||
keys: Dict[str, Any]
|
||||
) -> None:
|
||||
def interp(match):
|
||||
return str(keys[match.group(1)])
|
||||
sub_data = re.sub(r"\${([^}]+)}", interp, source_path.read_text())
|
||||
dest_path.write_text(sub_data)
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def ssl_certs() -> Iterator[Dict[str, pathlib.Path]]:
|
||||
with tempfile.TemporaryDirectory(prefix="moonraker-certs-") as tmpdir:
|
||||
tmp_path = pathlib.Path(tmpdir)
|
||||
cert_path = tmp_path.joinpath("certificate.pem")
|
||||
key_path = tmp_path.joinpath("privkey.pem")
|
||||
cmd = (
|
||||
f"openssl req -newkey rsa:4096 -nodes -keyout {key_path} "
|
||||
f"-x509 -days 365 -out {cert_path} -sha256 "
|
||||
"-subj '/C=US/ST=NRW/L=Earth/O=Moonraker/OU=IT/"
|
||||
"CN=www.moonraker-test.com/emailAddress=mail@moonraker-test.com'"
|
||||
)
|
||||
args = shlex.split(cmd)
|
||||
subprocess.run(args, check=True)
|
||||
yield {
|
||||
"ssl_certificate_path": cert_path,
|
||||
"ssl_key_path": key_path,
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def event_loop() -> Iterator[asyncio.AbstractEventLoop]:
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_args(ssl_certs: Dict[str, pathlib.Path]
|
||||
) -> Iterator[Dict[str, pathlib.Path]]:
|
||||
mconf_asset = ASSETS.joinpath(f"moonraker/base_server.conf")
|
||||
secrets_asset = ASSETS.joinpath(f"moonraker/secrets.ini")
|
||||
pcfg_asset = ASSETS.joinpath(f"klipper/base_printer.cfg")
|
||||
with tempfile.TemporaryDirectory(prefix="moonraker-test") as tmpdir:
|
||||
tmp_path = pathlib.Path(tmpdir)
|
||||
secrets_dest = tmp_path.joinpath("secrets.ini")
|
||||
shutil.copy(secrets_asset, secrets_dest)
|
||||
cfg_path = tmp_path.joinpath("config")
|
||||
cfg_path.mkdir()
|
||||
log_path = tmp_path.joinpath("logs")
|
||||
log_path.mkdir()
|
||||
db_path = tmp_path.joinpath("database")
|
||||
db_path.mkdir()
|
||||
gcode_path = tmp_path.joinpath("gcode_files")
|
||||
gcode_path.mkdir()
|
||||
dest_paths = {
|
||||
"temp_path": tmp_path,
|
||||
"asset_path": ASSETS,
|
||||
"config_path": cfg_path,
|
||||
"database_path": db_path,
|
||||
"log_path": log_path,
|
||||
"gcode_path": gcode_path,
|
||||
"secrets_path": secrets_dest,
|
||||
"klippy_uds_path": tmp_path.joinpath("klippy_uds"),
|
||||
"klippy_pty_path": tmp_path.joinpath("klippy_pty"),
|
||||
"klipper.dict": ASSETS.joinpath("klipper/klipper.dict"),
|
||||
"mconf_asset": mconf_asset,
|
||||
"pcfg_asset": pcfg_asset,
|
||||
}
|
||||
dest_paths.update(ssl_certs)
|
||||
mconf_dest = cfg_path.joinpath("moonraker.conf")
|
||||
dest_paths["moonraker.conf"] = mconf_dest
|
||||
interpolate_config(mconf_asset, mconf_dest, dest_paths)
|
||||
pcfg_dest = cfg_path.joinpath("printer.cfg")
|
||||
dest_paths["printer.cfg"] = pcfg_dest
|
||||
interpolate_config(pcfg_asset, pcfg_dest, dest_paths)
|
||||
yield dest_paths
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def klippy_session(session_args: Dict[str, pathlib.Path],
|
||||
pytestconfig: pytest.Config) -> Iterator[KlippyProcess]:
|
||||
pytestconfig.stash[need_klippy_restart] = False
|
||||
kpath = pytestconfig.getoption('klipper_path', "~/klipper")
|
||||
kexec = pytestconfig.getoption('klipper_exec', None)
|
||||
if kexec is None:
|
||||
kexec = sys.executable
|
||||
exec = pathlib.Path(kexec).expanduser()
|
||||
klipper_path = pathlib.Path(kpath).expanduser()
|
||||
base_cmd = f"{exec} {klipper_path}/klippy/klippy.py "
|
||||
kproc = KlippyProcess(base_cmd, session_args)
|
||||
kproc.start()
|
||||
yield kproc
|
||||
kproc.stop()
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def klippy(klippy_session: KlippyProcess,
|
||||
pytestconfig: pytest.Config):
|
||||
if pytestconfig.stash[need_klippy_restart]:
|
||||
pytestconfig.stash[need_klippy_restart] = False
|
||||
klippy_session.restart()
|
||||
return klippy_session
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def path_args(request: pytest.FixtureRequest,
|
||||
session_args: Dict[str, pathlib.Path],
|
||||
pytestconfig: pytest.Config
|
||||
) -> Iterator[Dict[str, pathlib.Path]]:
|
||||
path_marker = request.node.get_closest_marker("run_paths")
|
||||
paths: Dict[str, Any] = {
|
||||
"moonraker_conf": "base_server.conf",
|
||||
"secrets": "secrets.ini",
|
||||
"printer_cfg": "base_printer.cfg",
|
||||
"klippy_uds": None,
|
||||
}
|
||||
if path_marker is not None:
|
||||
paths.update(path_marker.kwargs)
|
||||
tmp_path = session_args["temp_path"]
|
||||
cfg_path = session_args["config_path"]
|
||||
mconf_dest = session_args["moonraker.conf"]
|
||||
mconf_asset = ASSETS.joinpath(f"moonraker/{paths['moonraker_conf']}")
|
||||
pcfg_asset = ASSETS.joinpath(f"klipper/{paths['printer_cfg']}")
|
||||
last_uds = session_args["klippy_uds_path"]
|
||||
if paths["klippy_uds"] is not None:
|
||||
tmp_uds = tmp_path.joinpath(paths["klippy_uds"])
|
||||
session_args["klippy_uds_path"] = tmp_uds
|
||||
if (
|
||||
not mconf_asset.samefile(session_args["mconf_asset"]) or
|
||||
paths["klippy_uds"] is not None
|
||||
):
|
||||
session_args['mconf_asset'] = mconf_asset
|
||||
interpolate_config(mconf_asset, mconf_dest, session_args)
|
||||
if not pcfg_asset.samefile(session_args["pcfg_asset"]):
|
||||
pcfg_dest = session_args["printer.cfg"]
|
||||
session_args["pcfg_asset"] = pcfg_asset
|
||||
interpolate_config(pcfg_asset, pcfg_dest, session_args)
|
||||
pytestconfig.stash[need_klippy_restart] = True
|
||||
if paths["secrets"] != session_args["secrets_path"].name:
|
||||
secrets_asset = ASSETS.joinpath(f"moonraker/{paths['secrets']}")
|
||||
secrets_dest = tmp_path.joinpath(paths['secrets'])
|
||||
shutil.copy(secrets_asset, secrets_dest)
|
||||
session_args["secrets_path"] = secrets_dest
|
||||
if "moonraker_log" in paths:
|
||||
log_path = session_args["log_path"]
|
||||
session_args['moonraker.log'] = log_path.joinpath(
|
||||
paths["moonraker_log"])
|
||||
bkp_dest: pathlib.Path = cfg_path.joinpath(".moonraker.conf.bkp")
|
||||
if "moonraker_bkp" in paths:
|
||||
bkp_source = ASSETS.joinpath("moonraker/base_server.conf")
|
||||
bkp_dest = cfg_path.joinpath(paths["moonraker_bkp"])
|
||||
interpolate_config(bkp_source, bkp_dest, session_args)
|
||||
if "database" in paths:
|
||||
db_source = ASSETS.joinpath(f"moonraker/{paths['database']}")
|
||||
db_dest = session_args["database_path"]
|
||||
db_args = {"input": str(db_source), "destination": db_dest}
|
||||
dbtool.restore(db_args)
|
||||
yield session_args
|
||||
log = session_args.pop("moonraker.log", None)
|
||||
if log is not None and log.is_file():
|
||||
log.unlink()
|
||||
if bkp_dest.is_file():
|
||||
bkp_dest.unlink()
|
||||
for item in session_args["database_path"].iterdir():
|
||||
if item.is_file():
|
||||
item.unlink()
|
||||
session_args["klippy_uds_path"] = last_uds
|
||||
if paths["klippy_uds"] is not None:
|
||||
# restore the original uds path
|
||||
interpolate_config(mconf_asset, mconf_dest, session_args)
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def base_server(path_args: Dict[str, pathlib.Path],
|
||||
event_loop: asyncio.AbstractEventLoop
|
||||
) -> Iterator[Server]:
|
||||
evtloop = EventLoop()
|
||||
args = {
|
||||
'config_file': str(path_args['moonraker.conf']),
|
||||
'log_file': str(path_args.get("moonraker.log", "")),
|
||||
'software_version': "moonraker-pytest"
|
||||
}
|
||||
ql = logger = None
|
||||
if args["log_file"]:
|
||||
ql, logger, warning = utils.setup_logging(args)
|
||||
if warning:
|
||||
args["log_warning"] = warning
|
||||
yield Server(args, logger, evtloop)
|
||||
if ql is not None:
|
||||
ql.stop()
|
||||
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def full_server(base_server: Server) -> AsyncIterator[Server]:
|
||||
base_server.load_components()
|
||||
ret = base_server.server_init(start_server=False)
|
||||
await asyncio.wait_for(ret, 4.)
|
||||
yield base_server
|
||||
if base_server.event_loop.aioloop.is_running():
|
||||
await base_server._stop_server(exit_reason="terminate")
|
||||
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def ready_server(full_server: Server, klippy: KlippyProcess):
|
||||
ret = full_server.start_server(connect_to_klippy=False)
|
||||
await asyncio.wait_for(ret, 4.)
|
||||
ret = full_server.klippy_connection.connect()
|
||||
await asyncio.wait_for(ret, 4.)
|
||||
yield full_server
|
||||
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def http_client() -> AsyncIterator[HttpClient]:
|
||||
client = HttpClient()
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def websocket_client(request: pytest.FixtureRequest
|
||||
) -> AsyncIterator[WebsocketClient]:
|
||||
conn_marker = request.node.get_closest_marker("no_ws_connect")
|
||||
client = WebsocketClient()
|
||||
if conn_marker is None:
|
||||
await client.connect()
|
||||
yield client
|
||||
client.close()
|
||||
5
tests/fixtures/__init__.py
vendored
Normal file
5
tests/fixtures/__init__.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from .klippy_process import KlippyProcess
|
||||
from .http_client import HttpClient
|
||||
from .websocket_client import WebsocketClient
|
||||
|
||||
__all__ = ("KlippyProcess", "HttpClient", "WebsocketClient")
|
||||
78
tests/fixtures/http_client.py
vendored
Normal file
78
tests/fixtures/http_client.py
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError
|
||||
from tornado.httputil import HTTPHeaders
|
||||
from tornado.escape import url_escape
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class HttpClient:
|
||||
error = HTTPError
|
||||
def __init__(self,
|
||||
type: str = "http",
|
||||
port: int = 7010
|
||||
) -> None:
|
||||
self.client = AsyncHTTPClient()
|
||||
assert type in ["http", "https"]
|
||||
self.prefix = f"{type}://127.0.0.1:{port}/"
|
||||
self.last_response_headers: HTTPHeaders = HTTPHeaders()
|
||||
|
||||
def get_response_headers(self) -> HTTPHeaders:
|
||||
return self.last_response_headers
|
||||
|
||||
async def _do_request(self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
ep = "/".join([url_escape(part, plus=False) for part in
|
||||
endpoint.lstrip("/").split("/")])
|
||||
url = self.prefix + ep
|
||||
method = method.upper()
|
||||
body: Optional[str] = "" if method == "POST" else None
|
||||
if args:
|
||||
if method in ["GET", "DELETE"]:
|
||||
parts = []
|
||||
for key, val in args.items():
|
||||
if isinstance(val, list):
|
||||
val = ",".join(val)
|
||||
if val:
|
||||
parts.append(f"{url_escape(key)}={url_escape(val)}")
|
||||
else:
|
||||
parts.append(url_escape(key))
|
||||
qs = "&".join(parts)
|
||||
url += "?" + qs
|
||||
else:
|
||||
body = json.dumps(args)
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
request = HTTPRequest(url, method, headers, body=body,
|
||||
request_timeout=2., connect_timeout=2.)
|
||||
ret = await self.client.fetch(request)
|
||||
self.last_response_headers = HTTPHeaders(ret.headers)
|
||||
return json.loads(ret.body)
|
||||
|
||||
async def get(self,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
return await self._do_request("GET", endpoint, args, headers)
|
||||
|
||||
async def post(self,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
return await self._do_request("POST", endpoint, args, headers)
|
||||
|
||||
async def delete(self,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
return await self._do_request("DELETE", endpoint, args, headers)
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
||||
81
tests/fixtures/klippy_process.py
vendored
Normal file
81
tests/fixtures/klippy_process.py
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import pathlib
|
||||
import shlex
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
class KlippyProcess:
|
||||
def __init__(self,
|
||||
base_cmd: str,
|
||||
path_args: Dict[str, pathlib.Path],
|
||||
) -> None:
|
||||
self.base_cmd = base_cmd
|
||||
self.config_path = path_args['printer.cfg']
|
||||
self.orig_config = self.config_path
|
||||
self.dict_path = path_args["klipper.dict"]
|
||||
self.pty_path = path_args["klippy_pty_path"]
|
||||
self.uds_path = path_args["klippy_uds_path"]
|
||||
self.proc: Optional[subprocess.Popen] = None
|
||||
self.fd: int = -1
|
||||
|
||||
def start(self):
|
||||
if self.proc is not None:
|
||||
return
|
||||
args = (
|
||||
f"{self.config_path} -o /dev/null -d {self.dict_path} "
|
||||
f"-a {self.uds_path} -I {self.pty_path}"
|
||||
)
|
||||
cmd = f"{self.base_cmd} {args}"
|
||||
cmd_parts = shlex.split(cmd)
|
||||
self.proc = subprocess.Popen(cmd_parts)
|
||||
for _ in range(250):
|
||||
if self.pty_path.exists():
|
||||
try:
|
||||
self.fd = os.open(
|
||||
str(self.pty_path), os.O_RDWR | os.O_NONBLOCK)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
time.sleep(.01)
|
||||
else:
|
||||
self.stop()
|
||||
pytest.fail("Unable to start Klippy process")
|
||||
return False
|
||||
return True
|
||||
|
||||
def send_gcode(self, gcode: str) -> None:
|
||||
if self.fd == -1:
|
||||
return
|
||||
try:
|
||||
os.write(self.fd, f"{gcode}\n".encode())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def restart(self):
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
if self.fd != -1:
|
||||
os.close(self.fd)
|
||||
self.fd = -1
|
||||
if self.proc is not None:
|
||||
self.proc.terminate()
|
||||
try:
|
||||
self.proc.wait(2.)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
self.proc = None
|
||||
|
||||
def get_paths(self) -> Dict[str, pathlib.Path]:
|
||||
return {
|
||||
"printer.cfg": self.config_path,
|
||||
"klipper.dict": self.dict_path,
|
||||
"klippy_uds_path": self.uds_path,
|
||||
"klippy_pty_path": self.pty_path,
|
||||
}
|
||||
136
tests/fixtures/websocket_client.py
vendored
Normal file
136
tests/fixtures/websocket_client.py
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import json
|
||||
import asyncio
|
||||
import tornado.websocket
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
Tuple,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Any,
|
||||
Optional,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tornado.websocket import WebSocketClientConnection
|
||||
|
||||
class WebsocketError(Exception):
|
||||
def __init__(self, code, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.code = code
|
||||
|
||||
class WebsocketClient:
|
||||
error = WebsocketError
|
||||
def __init__(self,
|
||||
type: str = "ws",
|
||||
port: int = 7010
|
||||
) -> None:
|
||||
self.ws: Optional[WebSocketClientConnection] = None
|
||||
self.pending_requests: Dict[int, asyncio.Future] = {}
|
||||
self.notify_cbs: Dict[str, List[Callable[..., None]]] = {}
|
||||
assert type in ["ws", "wss"]
|
||||
self.url = f"{type}://127.0.0.1:{port}/websocket"
|
||||
|
||||
async def connect(self, token: Optional[str] = None) -> None:
|
||||
url = self.url
|
||||
if token is not None:
|
||||
url += f"?token={token}"
|
||||
self.ws = await tornado.websocket.websocket_connect(
|
||||
url, connect_timeout=2.,
|
||||
on_message_callback=self._on_message_received)
|
||||
|
||||
async def request(self,
|
||||
remote_method: str,
|
||||
args: Dict[str, Any] = {}
|
||||
) -> Dict[str, Any]:
|
||||
if self.ws is None:
|
||||
pytest.fail("Websocket Not Connected")
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
req, req_id = self._encode_request(remote_method, args)
|
||||
self.pending_requests[req_id] = fut
|
||||
await self.ws.write_message(req)
|
||||
return await asyncio.wait_for(fut, 2.)
|
||||
|
||||
def _encode_request(self,
|
||||
method: str,
|
||||
args: Dict[str, Any]
|
||||
) -> Tuple[str, int]:
|
||||
request: Dict[str, Any] = {
|
||||
'jsonrpc': "2.0",
|
||||
'method': method,
|
||||
}
|
||||
if args:
|
||||
request['params'] = args
|
||||
req_id = id(request)
|
||||
request["id"] = req_id
|
||||
return json.dumps(request), req_id
|
||||
|
||||
def _on_message_received(self, message: Union[str, bytes, None]) -> None:
|
||||
if isinstance(message, str):
|
||||
self._decode_jsonrpc(message)
|
||||
|
||||
def _decode_jsonrpc(self, data: str) -> None:
|
||||
try:
|
||||
resp: Dict[str, Any] = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
pytest.fail(f"Websocket JSON Decode Error: {data}")
|
||||
header = resp.get('jsonrpc', "")
|
||||
if header != "2.0":
|
||||
# Invalid Json, set error if we can get the id
|
||||
pytest.fail(f"Invalid jsonrpc header: {data}")
|
||||
req_id: Optional[int] = resp.get("id")
|
||||
method: Optional[str] = resp.get("method")
|
||||
if method is not None:
|
||||
if req_id is None:
|
||||
params = resp.get("params", [])
|
||||
if not isinstance(params, list):
|
||||
pytest.fail("jsonrpc notification params"
|
||||
f"should always be a list: {data}")
|
||||
if method in self.notify_cbs:
|
||||
for func in self.notify_cbs[method]:
|
||||
func(*params)
|
||||
else:
|
||||
# This is a request from the server (should not happen)
|
||||
pytest.fail(f"Server should not request from client: {data}")
|
||||
elif req_id is not None:
|
||||
pending_fut = self.pending_requests.pop(req_id, None)
|
||||
if pending_fut is None:
|
||||
# No future pending for this response
|
||||
return
|
||||
# This is a response
|
||||
if "result" in resp:
|
||||
pending_fut.set_result(resp["result"])
|
||||
elif "error" in resp:
|
||||
err = resp["error"]
|
||||
try:
|
||||
code = err["code"]
|
||||
msg = err["message"]
|
||||
except Exception:
|
||||
pytest.fail(f"Invalid jsonrpc error: {data}")
|
||||
exc = WebsocketError(code, msg)
|
||||
pending_fut.set_exception(exc)
|
||||
else:
|
||||
pytest.fail(
|
||||
f"Invalid jsonrpc packet, no result or error: {data}")
|
||||
else:
|
||||
# Invalid json
|
||||
pytest.fail(f"Invalid jsonrpc packet, no id: {data}")
|
||||
|
||||
def register_notify_callback(self, name: str, callback) -> None:
|
||||
if name in self.notify_cbs:
|
||||
self.notify_cbs[name].append(callback)
|
||||
else:
|
||||
self.notify_cbs[name][callback]
|
||||
|
||||
def close(self):
|
||||
for fut in self.pending_requests.values():
|
||||
if not fut.done():
|
||||
fut.set_exception(WebsocketError(
|
||||
0, "Closing Websocket Client"))
|
||||
if self.ws is not None:
|
||||
self.ws.close(1000, "Test Complete")
|
||||
70
tests/mocks/__init__.py
Normal file
70
tests/mocks/__init__.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
from utils import ServerError
|
||||
from .mock_gpio import MockGpiod
|
||||
|
||||
__all__ = ("MockReader", "MockWriter", "MockComponent", "MockWebsocket",
|
||||
"MockGpiod")
|
||||
|
||||
class MockWriter:
|
||||
def __init__(self, wait_drain: bool = False) -> None:
|
||||
self.wait_drain = wait_drain
|
||||
|
||||
def write(self, data: str) -> None:
|
||||
pass
|
||||
|
||||
async def drain(self) -> None:
|
||||
if self.wait_drain:
|
||||
evt = asyncio.Event()
|
||||
await evt.wait()
|
||||
else:
|
||||
raise ServerError("TestError")
|
||||
|
||||
class MockReader:
|
||||
def __init__(self, action: str = "") -> None:
|
||||
self.action = action
|
||||
self.eof = False
|
||||
|
||||
def at_eof(self) -> bool:
|
||||
return self.eof
|
||||
|
||||
async def readuntil(self, stop: bytes) -> bytes:
|
||||
if self.action == "wait":
|
||||
evt = asyncio.Event()
|
||||
await evt.wait()
|
||||
return b""
|
||||
elif self.action == "raise_error":
|
||||
raise ServerError("TestError")
|
||||
else:
|
||||
self.eof = True
|
||||
return b"NotJsonDecodable"
|
||||
|
||||
|
||||
class MockComponent:
|
||||
def __init__(self,
|
||||
err_init: bool = False,
|
||||
err_exit: bool = False,
|
||||
err_close: bool = False
|
||||
) -> None:
|
||||
self.err_init = err_init
|
||||
self.err_exit = err_exit
|
||||
self.err_close = err_close
|
||||
|
||||
async def component_init(self):
|
||||
if self.err_init:
|
||||
raise ServerError("test")
|
||||
|
||||
async def on_exit(self):
|
||||
if self.err_exit:
|
||||
raise ServerError("test")
|
||||
|
||||
async def close(self):
|
||||
if self.err_close:
|
||||
raise ServerError("test")
|
||||
|
||||
class MockWebsocket:
|
||||
def __init__(self, fut: asyncio.Future) -> None:
|
||||
self.future = fut
|
||||
|
||||
def queue_message(self, data: str):
|
||||
self.future.set_result(data)
|
||||
193
tests/mocks/mock_gpio.py
Normal file
193
tests/mocks/mock_gpio.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Optional, List, Tuple
|
||||
|
||||
class GpioException(Exception):
|
||||
pass
|
||||
|
||||
class MockGpiod:
|
||||
LINE_REQ_DIR_OUT = 3
|
||||
LINE_REQ_EV_BOTH_EDGES = 6
|
||||
LINE_REQ_FLAG_ACTIVE_LOW = 1 << 2
|
||||
LINE_REQ_FLAG_BIAS_DISABLE = 1 << 3
|
||||
LINE_REQ_FLAG_BIAS_PULL_DOWN = 1 << 4
|
||||
LINE_REQ_FLAG_BIAS_PULL_UP = 1 << 5
|
||||
|
||||
def __init__(self, version: str = "1.2") -> None:
|
||||
self.version = version
|
||||
self.Chip = MockChipWrapper(self)
|
||||
self.LineEvent = MockLineEvent
|
||||
self.chips: Dict[str, MockChip] = {}
|
||||
|
||||
def version_string(self) -> str:
|
||||
return self.version
|
||||
|
||||
def version_tuple(self) -> Tuple[int, ...]:
|
||||
return tuple([int(v) for v in self.version.split(".")])
|
||||
|
||||
def get_chip(self, chip_name) -> Optional[MockChip]:
|
||||
return self.chips.get(chip_name, None)
|
||||
|
||||
def add_chip(self, chip: MockChip):
|
||||
self.chips[chip.name] = chip
|
||||
|
||||
def pop_chip(self, name: str):
|
||||
self.chips.pop(name, None)
|
||||
|
||||
def find_line(self, chip_id: str, pin_id: str) -> MockLine:
|
||||
if chip_id not in self.chips:
|
||||
raise GpioException(f"Unable to find chip {chip_id}")
|
||||
return self.chips[chip_id].find_line(pin_id)
|
||||
|
||||
class MockChipWrapper:
|
||||
OPEN_BY_NAME = 2
|
||||
def __init__(self, gpiod: MockGpiod) -> None:
|
||||
self.mock_gpiod = gpiod
|
||||
|
||||
def __call__(self, chip_name: str, flags: int) -> MockChip:
|
||||
if chip_name in self.mock_gpiod.chips:
|
||||
return self.mock_gpiod.chips[chip_name]
|
||||
chip = MockChip(chip_name, flags, self.mock_gpiod)
|
||||
self.mock_gpiod.add_chip(chip)
|
||||
return chip
|
||||
|
||||
class MockChip:
|
||||
def __init__(self,
|
||||
chip_name: str,
|
||||
flags: int,
|
||||
mock_gpiod: MockGpiod
|
||||
) -> None:
|
||||
self.name = chip_name
|
||||
self.flags = flags
|
||||
self.mock_gpiod = mock_gpiod
|
||||
self.requested_lines: Dict[str, MockLine] = {}
|
||||
|
||||
def get_line(self, pin_id: str) -> MockLine:
|
||||
if pin_id in self.requested_lines:
|
||||
raise GpioException(f"Line {pin_id} already reserved")
|
||||
line = MockLine(self, pin_id, self.mock_gpiod)
|
||||
self.requested_lines[pin_id] = line
|
||||
return line
|
||||
|
||||
def find_line(self, pin_id: str) -> MockLine:
|
||||
if pin_id not in self.requested_lines:
|
||||
raise GpioException(f"Unable to find line {pin_id}")
|
||||
return self.requested_lines[pin_id]
|
||||
|
||||
def pop_line(self, name: str) -> None:
|
||||
self.requested_lines.pop(name, None)
|
||||
|
||||
def close(self) -> None:
|
||||
for line in list(self.requested_lines.values()):
|
||||
line.release()
|
||||
self.requested_lines = {}
|
||||
self.mock_gpiod.pop_chip(self.name)
|
||||
|
||||
class MockLine:
|
||||
def __init__(self,
|
||||
chip: MockChip,
|
||||
name: str,
|
||||
mock_gpiod: MockGpiod
|
||||
) -> None:
|
||||
self.mock_gpiod = mock_gpiod
|
||||
self.chip = chip
|
||||
self.name = name
|
||||
self.consumer_name: str = ""
|
||||
self.is_event = False
|
||||
self.invert = False
|
||||
self.value = 0
|
||||
self.read_pipe: Optional[int] = None
|
||||
self.write_pipe: Optional[int] = None
|
||||
self.bias = "not_configured"
|
||||
|
||||
def request(self,
|
||||
consumer: str,
|
||||
type: int,
|
||||
flags: int = 0,
|
||||
default_vals: Optional[List[int]] = None,
|
||||
default_val: Optional[int] = None
|
||||
) -> None:
|
||||
self.consumer_name = consumer
|
||||
version = self.mock_gpiod.version_tuple()
|
||||
if type == MockGpiod.LINE_REQ_DIR_OUT:
|
||||
self.is_event = False
|
||||
if default_vals is not None:
|
||||
if version > (1, 2):
|
||||
logging.warn("default_vals is deprecated in gpiod 1.3+")
|
||||
self.value = default_vals[0]
|
||||
elif default_val is not None:
|
||||
if version < (1, 3):
|
||||
raise GpioException(
|
||||
"default_val not available in gpiod < 1.3")
|
||||
self.value = default_val
|
||||
elif type == MockGpiod.LINE_REQ_EV_BOTH_EDGES:
|
||||
self.is_event = True
|
||||
if version >= (1, 5):
|
||||
if flags & MockGpiod.LINE_REQ_FLAG_BIAS_DISABLE:
|
||||
self.bias = "disabled"
|
||||
elif flags & MockGpiod.LINE_REQ_FLAG_BIAS_PULL_DOWN:
|
||||
self.bias = "pulldown"
|
||||
elif flags & MockGpiod.LINE_REQ_FLAG_BIAS_PULL_UP:
|
||||
self.bias = "pullup"
|
||||
self.read_pipe, self.write_pipe = os.pipe2(os.O_NONBLOCK)
|
||||
else:
|
||||
raise GpioException("Unsupported GPIO Type")
|
||||
if flags & MockGpiod.LINE_REQ_FLAG_ACTIVE_LOW:
|
||||
self.invert = True
|
||||
|
||||
def release(self) -> None:
|
||||
if self.read_pipe is not None:
|
||||
try:
|
||||
os.close(self.read_pipe)
|
||||
except Exception:
|
||||
pass
|
||||
if self.write_pipe is not None:
|
||||
try:
|
||||
os.close(self.write_pipe)
|
||||
except Exception:
|
||||
pass
|
||||
self.chip.pop_line(self.name)
|
||||
|
||||
def set_value(self, value: int) -> None:
|
||||
if self.is_event:
|
||||
raise GpioException("Cannot set the value for an input pin")
|
||||
self.value = int(not not value)
|
||||
|
||||
def get_value(self) -> int:
|
||||
return self.value
|
||||
|
||||
def event_read(self) -> MockLineEvent:
|
||||
if self.read_pipe is None:
|
||||
raise GpioException
|
||||
try:
|
||||
data = os.read(self.read_pipe, 64)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
value = int(not not data[-1])
|
||||
self.value = value
|
||||
return MockLineEvent(self.value)
|
||||
|
||||
def event_get_fd(self) -> int:
|
||||
if self.read_pipe is None:
|
||||
raise GpioException("Event not configured")
|
||||
return self.read_pipe
|
||||
|
||||
def simulate_line_event(self, value: int) -> None:
|
||||
if self.write_pipe is None:
|
||||
raise GpioException("Event not configured")
|
||||
val = bytes([int(not not value)])
|
||||
try:
|
||||
os.write(self.write_pipe, val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class MockLineEvent:
|
||||
RISING_EDGE = 1
|
||||
FALLING_EDGE = 2
|
||||
def __init__(self, value: int) -> None:
|
||||
if value == 1:
|
||||
self.type = self.RISING_EDGE
|
||||
else:
|
||||
self.type = self.FALLING_EDGE
|
||||
496
tests/test_config.py
Normal file
496
tests/test_config.py
Normal file
@@ -0,0 +1,496 @@
|
||||
from __future__ import annotations
|
||||
import pathlib
|
||||
import pytest
|
||||
import hashlib
|
||||
import confighelper
|
||||
import shutil
|
||||
import time
|
||||
from confighelper import ConfigError
|
||||
from moonraker import Server
|
||||
from utils import ServerError
|
||||
from components import gpio
|
||||
from mocks import MockGpiod
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
if TYPE_CHECKING:
|
||||
from confighelper import ConfigHelper
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def config(base_server: Server) -> ConfigHelper:
|
||||
base_server.load_component(base_server.config, "secrets")
|
||||
return base_server.config
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def test_config(config: ConfigHelper,
|
||||
path_args: Dict[str, pathlib.Path]
|
||||
) -> ConfigHelper:
|
||||
assets = path_args['asset_path']
|
||||
sup_cfg_path = assets.joinpath("moonraker/supplemental.conf")
|
||||
if not sup_cfg_path.exists():
|
||||
pytest.fail("Supplemental config not found")
|
||||
cfg = config.read_supplemental_config(str(sup_cfg_path))
|
||||
return cfg["test_options"]
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def gpio_config(test_config: ConfigHelper,
|
||||
monkeypatch: pytest.MonkeyPatch
|
||||
) -> ConfigHelper:
|
||||
def load_gpio_mock(name: str) -> MockGpiod:
|
||||
return MockGpiod()
|
||||
monkeypatch.setattr(gpio, "load_system_module", load_gpio_mock)
|
||||
yield test_config
|
||||
server = test_config.get_server()
|
||||
gpio_comp = server.lookup_component("gpio", None)
|
||||
if gpio_comp is not None:
|
||||
gpio_comp.close()
|
||||
gpio_comp.reserved_gpios = {}
|
||||
|
||||
class TestConfigGeneric:
|
||||
def test_get_server(self, config: ConfigHelper):
|
||||
server = config.get_server()
|
||||
assert isinstance(server, Server)
|
||||
|
||||
def test_get_item(self, config: ConfigHelper):
|
||||
sec = config["file_manager"]
|
||||
assert sec.section == "file_manager"
|
||||
|
||||
def test_no_section_fail(self, config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
config["not_available"].get("no_section")
|
||||
|
||||
def test_contains(self, config: ConfigHelper):
|
||||
assert "file_manager" in config
|
||||
|
||||
def test_not_contains(self, config: ConfigHelper):
|
||||
assert "not_available" not in config
|
||||
|
||||
def test_has_option(self, config: ConfigHelper):
|
||||
assert config.has_option("host")
|
||||
|
||||
def test_get_name(self, config: ConfigHelper):
|
||||
assert config.get_name() == "server"
|
||||
|
||||
def test_get_options(self,
|
||||
config: ConfigHelper,
|
||||
path_args: Dict[str, pathlib.Path]):
|
||||
expected = {
|
||||
"host": "0.0.0.0",
|
||||
"port": "7010",
|
||||
"ssl_port": "7011",
|
||||
"klippy_uds_address": str(path_args["klippy_uds_path"])
|
||||
}
|
||||
assert expected == config.get_options()
|
||||
|
||||
def test_get_hash(self, config: ConfigHelper):
|
||||
opts = config.get_options()
|
||||
expected_hash = hashlib.sha256()
|
||||
for opt, val in opts.items():
|
||||
expected_hash.update(opt.encode())
|
||||
expected_hash.update(val.encode())
|
||||
cfg_hash = config.get_hash().hexdigest()
|
||||
assert cfg_hash == expected_hash.hexdigest()
|
||||
|
||||
def test_missing_supplemental_config(config: ConfigHelper):
|
||||
no_file = pathlib.Path("nofile")
|
||||
with pytest.raises(ConfigError):
|
||||
config.read_supplemental_config(no_file)
|
||||
|
||||
def test_error_supplemental_config(config: ConfigHelper,
|
||||
path_args: Dict[str, pathlib.Path]):
|
||||
assets = path_args["asset_path"]
|
||||
invalid_cfg = assets.joinpath("moonraker/invalid_config.conf")
|
||||
if not invalid_cfg.exists():
|
||||
pytest.fail("Invalid Config File does not exist")
|
||||
with pytest.raises(ConfigError):
|
||||
config.read_supplemental_config(invalid_cfg)
|
||||
|
||||
def test_prefix_sections(test_config: ConfigHelper):
|
||||
prefix = test_config.get_prefix_sections("prefix_sec")
|
||||
expected = ["prefix_sec one", "prefix_sec two", "prefix_sec three"]
|
||||
assert prefix == expected
|
||||
|
||||
class TestGetString:
|
||||
def test_get_str_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.get("test_string")
|
||||
assert val == "Hello World"
|
||||
|
||||
def test_get_str_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.get("invalid_option")
|
||||
|
||||
def test_get_str_default(self, test_config: ConfigHelper):
|
||||
assert test_config.get("invalid_option", None) is None
|
||||
|
||||
def test_get_str_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.get("test_string", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_string' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetInt:
|
||||
def test_get_int_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.getint("test_int")
|
||||
assert val == 1
|
||||
|
||||
def test_get_int_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getint("invalid_option")
|
||||
|
||||
def test_get_int_default(self, test_config: ConfigHelper):
|
||||
assert test_config.getint("invalid_option", None) is None
|
||||
|
||||
def test_get_int_fail_above(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getint("test_int", above=1)
|
||||
|
||||
def test_get_int_fail_below(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getint("test_int", below=1)
|
||||
|
||||
def test_get_int_fail_minval(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getint("test_int", minval=2)
|
||||
|
||||
def test_get_int_fail_maxval(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getint("test_int", maxval=0)
|
||||
|
||||
def test_get_int_pass_all(self, test_config: ConfigHelper):
|
||||
val = test_config.getint("test_int", above=0, below=2,
|
||||
minval=1, maxval=1)
|
||||
assert val == 1
|
||||
|
||||
def test_get_int_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.getint("test_int", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_int' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetFloat:
|
||||
def test_get_float_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.getfloat("test_float")
|
||||
assert 3.5 == pytest.approx(val)
|
||||
|
||||
def test_get_float_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getfloat("invalid_option")
|
||||
|
||||
def test_get_float_default(self, test_config: ConfigHelper):
|
||||
assert test_config.getfloat("invalid_option", None) is None
|
||||
|
||||
def test_get_float_fail_above(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getfloat("test_float", above=3.55)
|
||||
|
||||
def test_get_float_fail_below(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getfloat("test_float", below=3.45)
|
||||
|
||||
def test_get_float_fail_minval(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getfloat("test_float", minval=3.6)
|
||||
|
||||
def test_get_float_fail_maxval(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getfloat("test_float", maxval=3.45)
|
||||
|
||||
def test_get_float_pass_all(self, test_config: ConfigHelper):
|
||||
val = test_config.getfloat("test_float", above=3.45, below=3.55,
|
||||
minval=3, maxval=4)
|
||||
assert 3.5 == pytest.approx(val)
|
||||
|
||||
def test_get_float_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.getfloat("test_float", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_float' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetBoolean:
|
||||
def test_get_boolean_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.getboolean("test_bool")
|
||||
assert val is True
|
||||
|
||||
def test_get_float_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getboolean("invalid_option")
|
||||
|
||||
def test_get_float_default(self, test_config: ConfigHelper):
|
||||
assert test_config.getboolean("invalid_option", None) is None
|
||||
|
||||
def test_get_int_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.getboolean("test_bool", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_bool' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetList:
|
||||
def test_get_list_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.getlist("test_list")
|
||||
assert val == ["one", "two", "three"]
|
||||
|
||||
def test_get_list_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getlist("invalid_option")
|
||||
|
||||
def test_get_list_default(self, test_config: ConfigHelper):
|
||||
assert test_config.getlist("invalid_option", None) is None
|
||||
|
||||
def test_get_int_list(self, test_config: ConfigHelper):
|
||||
val = test_config.getintlist("test_int_list", separator=",")
|
||||
assert val == [1, 2, 3]
|
||||
|
||||
def test_get_float_list(self, test_config: ConfigHelper):
|
||||
val = test_config.getfloatlist("test_float_list", separator=",")
|
||||
assert val == pytest.approx([1.5, 2.8, 3.2])
|
||||
|
||||
def test_get_multi_list(self, test_config: ConfigHelper):
|
||||
val = test_config.getlists("test_multi_list", list_type=int,
|
||||
separators=("\n", ","))
|
||||
assert val == [[1, 2, 3], [4, 5, 6]]
|
||||
|
||||
def test_get_list_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.getlist("test_list", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_list' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetDict:
|
||||
def test_get_dict_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.getdict("test_dict", dict_type=int)
|
||||
assert val == {"one": 1, "two": 2, "three": 3}
|
||||
|
||||
def test_get_dict_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getdict("invalid_option")
|
||||
|
||||
def test_get_dict_default(self, test_config: ConfigHelper):
|
||||
assert test_config.getdict("invalid_option", None) is None
|
||||
|
||||
def test_get_dict_empty_fields(self, test_config: ConfigHelper):
|
||||
val = test_config.getdict("test_dict_empty_field",
|
||||
allow_empty_fields=True)
|
||||
assert val == {"one": "test", "two": None, "three": None}
|
||||
|
||||
def test_get_dict_empty_fields_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.getdict("test_dict_empty_field")
|
||||
|
||||
def test_get_dict_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.getdict("test_dict", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_dict' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetTemplate:
|
||||
def test_get_template_exists(self, test_config: ConfigHelper):
|
||||
val = test_config.gettemplate("test_template").render()
|
||||
assert val == "mqttuser"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_template_async(self, test_config: ConfigHelper):
|
||||
templ = test_config.gettemplate("test_template", is_async=True)
|
||||
val = await templ.render_async()
|
||||
assert val == "mqttuser"
|
||||
|
||||
def test_get_template_plain(self, test_config: ConfigHelper):
|
||||
val = test_config.gettemplate("test_string").render()
|
||||
assert val == "Hello World"
|
||||
|
||||
def test_get_template_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
test_config.gettemplate("invalid_option")
|
||||
|
||||
def test_get_template_render_fail(self, test_config: ConfigHelper):
|
||||
with pytest.raises(ServerError):
|
||||
test_config.gettemplate("test_template", is_async=True).render()
|
||||
|
||||
def test_get_template_default(self, test_config: ConfigHelper):
|
||||
assert test_config.gettemplate("invalid_option", None) is None
|
||||
|
||||
def test_load_template(self, test_config: ConfigHelper):
|
||||
val = test_config.load_template("test_template").render()
|
||||
assert val == "mqttuser"
|
||||
|
||||
def test_load_template_default(self, test_config: ConfigHelper):
|
||||
templ = test_config.load_template(
|
||||
"invalid_option", "{secrets.mqtt_credentials.password}")
|
||||
val = templ.render()
|
||||
assert val == "mqttpass"
|
||||
|
||||
def test_get_template_deprecate(self, test_config: ConfigHelper):
|
||||
server = test_config.get_server()
|
||||
test_config.gettemplate("test_template", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_template' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetGpioOut:
|
||||
def test_get_gpio_exists(self, gpio_config: ConfigHelper):
|
||||
val: gpio.GpioOutputPin = gpio_config.getgpioout("test_gpio")
|
||||
assert (
|
||||
val.orig == "gpiochip0/gpio26" and
|
||||
val.name == "gpiochip0:gpio26" and
|
||||
val.inverted is False and
|
||||
val.value == 0
|
||||
)
|
||||
|
||||
def test_get_gpio_no_chip(self, gpio_config: ConfigHelper):
|
||||
val: gpio.GpioOutputPin = gpio_config.getgpioout("test_gpio_no_chip")
|
||||
assert (
|
||||
val.orig == "gpio26" and
|
||||
val.name == "gpiochip0:gpio26" and
|
||||
val.inverted is False and
|
||||
val.value == 0
|
||||
)
|
||||
|
||||
def test_get_gpio_invert(self, gpio_config: ConfigHelper):
|
||||
val: gpio.GpioOutputPin = gpio_config.getgpioout("test_gpio_invert")
|
||||
assert (
|
||||
val.orig == "!gpiochip0/gpio26" and
|
||||
val.name == "gpiochip0:gpio26" and
|
||||
val.inverted is True and
|
||||
val.value == 0
|
||||
)
|
||||
|
||||
def test_get_gpio_no_chip_invert(self, gpio_config: ConfigHelper):
|
||||
val: gpio.GpioOutputPin = gpio_config.getgpioout(
|
||||
"test_gpio_no_chip_invert")
|
||||
assert (
|
||||
val.orig == "!gpio26" and
|
||||
val.name == "gpiochip0:gpio26" and
|
||||
val.inverted is True and
|
||||
val.value == 0
|
||||
)
|
||||
|
||||
def test_get_gpio_initial_value(self, gpio_config: ConfigHelper):
|
||||
val: gpio.GpioOutputPin = gpio_config.getgpioout(
|
||||
"test_gpio", initial_value=1)
|
||||
assert (
|
||||
val.orig == "gpiochip0/gpio26" and
|
||||
val.name == "gpiochip0:gpio26" and
|
||||
val.inverted is False and
|
||||
val.value == 1
|
||||
)
|
||||
|
||||
def test_get_gpio_fail(self, gpio_config: ConfigHelper):
|
||||
with pytest.raises(ConfigError):
|
||||
gpio_config.getgpioout("invalid_option")
|
||||
|
||||
def test_get_gpio_default(self, gpio_config: ConfigHelper):
|
||||
assert gpio_config.getgpioout("invalid_option", None) is None
|
||||
|
||||
@pytest.mark.parametrize("opt", ["pullup", "pullup_no_chip",
|
||||
"pulldown", "pulldown_no_chip"])
|
||||
def test_get_gpio_invalid(self, gpio_config: ConfigHelper, opt: str):
|
||||
option = f"test_gpio_{opt}"
|
||||
if not gpio_config.has_option(option):
|
||||
pytest.fail(f"No option {option}")
|
||||
with pytest.raises(ConfigError):
|
||||
gpio_config.getgpioout(option)
|
||||
|
||||
def test_get_gpio_deprecated(self, gpio_config: ConfigHelper):
|
||||
server = gpio_config.get_server()
|
||||
gpio_config.getgpioout("test_gpio", deprecate=True)
|
||||
expected = (
|
||||
f"[test_options]: Option 'test_gpio' is "
|
||||
"deprecated, see the configuration documention "
|
||||
"at https://moonraker.readthedocs.io/en/latest/configuration"
|
||||
)
|
||||
assert expected in server.warnings
|
||||
|
||||
class TestGetConfiguration:
|
||||
def test_get_config_no_exist(self, base_server: Server):
|
||||
fake_path = pathlib.Path("no_exist")
|
||||
if fake_path.exists():
|
||||
pytest.fail("Path exists")
|
||||
args = dict(base_server.app_args)
|
||||
args["config_file"] = str(fake_path)
|
||||
with pytest.raises(ConfigError):
|
||||
confighelper.get_configuration(base_server, args)
|
||||
|
||||
def test_get_config_no_access(self,
|
||||
base_server: Server,
|
||||
path_args: Dict[str, pathlib.Path]
|
||||
):
|
||||
cfg_path = path_args["config_path"]
|
||||
test_cfg = cfg_path.joinpath("test.conf")
|
||||
shutil.copy(path_args["moonraker.conf"], test_cfg)
|
||||
test_cfg.chmod(mode=222)
|
||||
args = dict(base_server.app_args)
|
||||
args["config_file"] = str(test_cfg)
|
||||
with pytest.raises(ConfigError):
|
||||
confighelper.get_configuration(base_server, args)
|
||||
|
||||
def test_get_config_no_server(self,
|
||||
base_server: Server,
|
||||
path_args: Dict[str, pathlib.Path]
|
||||
):
|
||||
assets = path_args['asset_path']
|
||||
sup_cfg_path = assets.joinpath("moonraker/supplemental.conf")
|
||||
if not sup_cfg_path.exists():
|
||||
pytest.fail("Supplemental config not found")
|
||||
args = dict(base_server.app_args)
|
||||
args["config_file"] = str(sup_cfg_path)
|
||||
with pytest.raises(ConfigError):
|
||||
confighelper.get_configuration(base_server, args)
|
||||
|
||||
class TestBackupConfig:
|
||||
def test_find_backup_fail(self):
|
||||
fake_path = pathlib.Path("no_exist")
|
||||
if fake_path.exists():
|
||||
fake_path.unlink()
|
||||
result = confighelper.find_config_backup(fake_path)
|
||||
assert result is None
|
||||
|
||||
def test_backup_config_success(
|
||||
self, path_args: Dict[str, pathlib.Path], config: ConfigHelper
|
||||
):
|
||||
cfg_path = path_args["moonraker.conf"]
|
||||
bkp_dest = cfg_path.parent.joinpath(f".{cfg_path.name}.bkp")
|
||||
if bkp_dest.exists():
|
||||
pytest.fail("Backup Already Exists")
|
||||
config.create_backup()
|
||||
assert bkp_dest.is_file()
|
||||
|
||||
def test_backup_skip(
|
||||
self, path_args: Dict[str, pathlib.Path], config: ConfigHelper
|
||||
):
|
||||
cfg_path = path_args["moonraker.conf"]
|
||||
bkp_dest = cfg_path.parent.joinpath(f".{cfg_path.name}.bkp")
|
||||
if not bkp_dest.exists():
|
||||
pytest.fail("Backup Not Present")
|
||||
stat = bkp_dest.stat()
|
||||
config.create_backup()
|
||||
assert stat == bkp_dest.stat()
|
||||
|
||||
def test_find_backup(self, path_args: Dict[str, pathlib.Path]):
|
||||
cfg_path = path_args["moonraker.conf"]
|
||||
bkp_dest = cfg_path.parent.joinpath(f".{cfg_path.name}.bkp")
|
||||
bkp = confighelper.find_config_backup(str(cfg_path))
|
||||
assert bkp == str(bkp_dest)
|
||||
1461
tests/test_database.py
Normal file
1461
tests/test_database.py
Normal file
File diff suppressed because it is too large
Load Diff
260
tests/test_klippy_connection.py
Normal file
260
tests/test_klippy_connection.py
Normal file
@@ -0,0 +1,260 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import asyncio
|
||||
import pathlib
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
from moonraker import ServerError
|
||||
from klippy_connection import KlippyRequest
|
||||
from mocks import MockReader, MockWriter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from moonraker import Server
|
||||
from conftest import KlippyProcess
|
||||
|
||||
@pytest.mark.usefixtures("klippy")
|
||||
@pytest.mark.asyncio
|
||||
async def test_klippy_startup(full_server: Server):
|
||||
evtloop = full_server.get_event_loop()
|
||||
futs = [evtloop.create_future() for _ in range(3)]
|
||||
events = {
|
||||
"server:klippy_identified": lambda: futs[0].set_result("id"),
|
||||
"server:klippy_started": lambda x: futs[1].set_result("started"),
|
||||
"server:klippy_ready": lambda: futs[2].set_result("ready")
|
||||
}
|
||||
for name, func in events.items():
|
||||
full_server.register_event_handler(name, func)
|
||||
await full_server.start_server()
|
||||
ret = await asyncio.wait_for(asyncio.gather(*futs), 4.)
|
||||
assert (
|
||||
ret == ["id", "started", "ready"] and
|
||||
full_server.klippy_connection.is_connected()
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gcode_response(ready_server: Server,
|
||||
klippy: KlippyProcess):
|
||||
evtloop = ready_server.get_event_loop()
|
||||
fut = evtloop.create_future()
|
||||
|
||||
def on_gc_resp(resp: str):
|
||||
if not fut.done():
|
||||
fut.set_result(resp)
|
||||
ready_server.register_event_handler("server:gcode_response", on_gc_resp)
|
||||
klippy.send_gcode("M118 Moonraker Test")
|
||||
await asyncio.wait_for(fut, 1.)
|
||||
assert "Moonraker Test" in fut.result()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_klippy_shutdown(ready_server: Server, klippy: KlippyProcess):
|
||||
evtloop = ready_server.get_event_loop()
|
||||
fut = evtloop.create_future()
|
||||
|
||||
def on_shutdown():
|
||||
if not fut.done():
|
||||
fut.set_result("shutdown")
|
||||
ready_server.register_event_handler("server:klippy_shutdown", on_shutdown)
|
||||
klippy.send_gcode("M112")
|
||||
await asyncio.wait_for(fut, 2.)
|
||||
assert fut.result() == "shutdown"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_klippy_reconnect(ready_server: Server, klippy: KlippyProcess):
|
||||
evtloop = ready_server.get_event_loop()
|
||||
futs = [evtloop.create_future() for _ in range(2)]
|
||||
events = {
|
||||
"server:klippy_disconnect": lambda: futs[0].set_result("disconnect"),
|
||||
"server:klippy_ready": lambda: futs[1].set_result("ready")
|
||||
}
|
||||
for name, func in events.items():
|
||||
ready_server.register_event_handler(name, func)
|
||||
klippy.restart()
|
||||
ret = await asyncio.wait_for(asyncio.gather(*futs), 6.)
|
||||
assert ret == ["disconnect", "ready"]
|
||||
|
||||
@pytest.mark.run_paths(klippy_uds="fake_uds")
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_klippy_connection_error(full_server: Server):
|
||||
await full_server.start_server()
|
||||
with pytest.raises(ServerError):
|
||||
kapis = full_server.klippy_connection.klippy_apis
|
||||
await kapis.run_gcode("M115")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_update(ready_server: Server, klippy: KlippyProcess):
|
||||
evtloop = ready_server.get_event_loop()
|
||||
fut = evtloop.create_future()
|
||||
|
||||
def on_status_update(data):
|
||||
if not fut.done():
|
||||
fut.set_result(data)
|
||||
ready_server.register_event_handler("server:status_update",
|
||||
on_status_update)
|
||||
kapis = ready_server.klippy_connection.klippy_apis
|
||||
await kapis.subscribe_objects({"toolhead": None})
|
||||
klippy.send_gcode("G28")
|
||||
await asyncio.wait_for(fut, 2.)
|
||||
assert isinstance(fut.result(), dict)
|
||||
|
||||
@pytest.mark.run_paths(printer_cfg="error_printer.cfg")
|
||||
@pytest.mark.asyncio
|
||||
async def test_klippy_error(ready_server: Server):
|
||||
kconn = ready_server.klippy_connection
|
||||
assert kconn.state == "error"
|
||||
|
||||
@pytest.mark.run_paths(printer_cfg="missing_reqs.cfg")
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_reqs(ready_server: Server):
|
||||
mreqs = sorted(ready_server.klippy_connection.missing_requirements)
|
||||
expected = ["display_status", "pause_resume", "virtual_sdcard"]
|
||||
assert mreqs == expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_close(full_server: Server):
|
||||
await full_server.start_server()
|
||||
# Test multiple close attempts, the second to enter
|
||||
# should wait and exit
|
||||
ret = full_server.klippy_connection.close(True)
|
||||
ret2 = full_server.klippy_connection.close(True)
|
||||
await asyncio.wait_for(asyncio.gather(ret, ret2), 4.)
|
||||
kconn = full_server.klippy_connection
|
||||
assert kconn.connection_task.cancelled()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_error(base_server: Server):
|
||||
base_server.server_running = True
|
||||
kconn = base_server.klippy_connection
|
||||
|
||||
def mock_is_connected():
|
||||
return kconn.init_attempts < 3
|
||||
kconn.is_connected = mock_is_connected
|
||||
ret = await kconn._init_klippy_connection()
|
||||
assert ret is False
|
||||
|
||||
def test_connect_fail(base_server: Server):
|
||||
ret = base_server.klippy_connection.connect()
|
||||
assert ret.result() is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_connect_fail(base_server: Server):
|
||||
ret = await base_server.klippy_connection.wait_connected()
|
||||
assert ret is False
|
||||
|
||||
@pytest.mark.run_paths(klippy_uds="fake_uds")
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_uds(base_server: Server):
|
||||
attempts = [1, 2, 3]
|
||||
|
||||
def mock_is_running():
|
||||
attempts.pop(0)
|
||||
return len(attempts) > 0
|
||||
base_server.is_running = mock_is_running
|
||||
ret = await base_server.klippy_connection._do_connect()
|
||||
assert ret is False
|
||||
|
||||
@pytest.mark.run_paths(klippy_uds="fake_uds")
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_uds_access(base_server: Server,
|
||||
path_args: Dict[str, pathlib.Path]):
|
||||
attempts = [1, 2, 3]
|
||||
uds_path = path_args['klippy_uds_path']
|
||||
uds_path.write_text("test")
|
||||
uds_path.chmod(mode=222)
|
||||
|
||||
def mock_is_running():
|
||||
attempts.pop(0)
|
||||
return len(attempts) > 0
|
||||
base_server.is_running = mock_is_running
|
||||
ret = await base_server.klippy_connection._do_connect()
|
||||
assert ret is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_not_connected(base_server: Server):
|
||||
req = KlippyRequest("", {})
|
||||
kconn = base_server.klippy_connection
|
||||
await kconn._write_request(req)
|
||||
assert isinstance(req.response, ServerError)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_error(base_server: Server):
|
||||
req = KlippyRequest("", {})
|
||||
kconn = base_server.klippy_connection
|
||||
kconn.writer = MockWriter()
|
||||
await kconn._write_request(req)
|
||||
assert isinstance(req.response, ServerError)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_cancelled(base_server: Server):
|
||||
req = KlippyRequest("", {})
|
||||
kconn = base_server.klippy_connection
|
||||
kconn.writer = MockWriter(wait_drain=True)
|
||||
task = base_server.event_loop.create_task(kconn._write_request(req))
|
||||
base_server.event_loop.delay_callback(.01, task.cancel)
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_error(base_server: Server,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
mock_reader = MockReader("raise_error")
|
||||
kconn = base_server.klippy_connection
|
||||
await kconn._read_stream(mock_reader)
|
||||
assert "Klippy Stream Read Error" == caplog.messages[-1]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_cancelled(base_server: Server):
|
||||
mock_reader = MockReader("wait")
|
||||
kconn = base_server.klippy_connection
|
||||
task = base_server.event_loop.create_task(
|
||||
kconn._read_stream(mock_reader))
|
||||
base_server.event_loop.delay_callback(.01, task.cancel)
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_decode_error(base_server: Server,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
mock_reader = MockReader()
|
||||
kconn = base_server.klippy_connection
|
||||
await kconn._read_stream(mock_reader)
|
||||
assert "Error processing Klippy Host Response:" in caplog.messages[-1]
|
||||
|
||||
def test_process_unknown_method(base_server: Server,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
cmd = {"method": "test_unknown"}
|
||||
kconn = base_server.klippy_connection
|
||||
kconn._process_command(cmd)
|
||||
assert "Unknown method received: test_unknown" == caplog.messages[-1]
|
||||
|
||||
def test_process_unknown_request(base_server: Server,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
cmd = {"id": 4543}
|
||||
kconn = base_server.klippy_connection
|
||||
kconn._process_command(cmd)
|
||||
expected = f"No request matching request ID: 4543, response: {cmd}"
|
||||
assert expected == caplog.messages[-1]
|
||||
|
||||
def test_process_invalid_request(base_server: Server):
|
||||
req = KlippyRequest("", {})
|
||||
kconn = base_server.klippy_connection
|
||||
kconn.pending_requests[req.id] = req
|
||||
cmd = {"id": req.id}
|
||||
kconn._process_command(cmd)
|
||||
assert isinstance(req.response, ServerError)
|
||||
|
||||
# TODO: This can probably go in a class with test apis
|
||||
@pytest.mark.asyncio
|
||||
async def test_call_remote_method(base_server: Server,
|
||||
klippy: KlippyProcess):
|
||||
fut = base_server.get_event_loop().create_future()
|
||||
|
||||
def method_test(result):
|
||||
fut.set_result(result)
|
||||
base_server.register_remote_method("moonraker_test", method_test)
|
||||
base_server.load_components()
|
||||
await base_server.server_init()
|
||||
ret = base_server.klippy_connection.wait_connected()
|
||||
await asyncio.wait_for(ret, 4.)
|
||||
klippy.send_gcode("TEST_REMOTE_METHOD")
|
||||
await fut
|
||||
await base_server._stop_server("terminate")
|
||||
assert fut.result() == "test"
|
||||
518
tests/test_server.py
Normal file
518
tests/test_server.py
Normal file
@@ -0,0 +1,518 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import asyncio
|
||||
import socket
|
||||
import pathlib
|
||||
from collections import namedtuple
|
||||
|
||||
from moonraker import CORE_COMPONENTS, Server, API_VERSION
|
||||
from moonraker import main as servermain
|
||||
from eventloop import EventLoop
|
||||
from utils import ServerError
|
||||
from confighelper import ConfigError
|
||||
from components.klippy_apis import KlippyAPI
|
||||
from mocks import MockComponent, MockWebsocket
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
AsyncIterator,
|
||||
Dict,
|
||||
Optional
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fixtures import HttpClient, WebsocketClient
|
||||
|
||||
MockArgs = namedtuple('MockArgs', ["logfile", "nologfile", "configfile"])
|
||||
|
||||
@pytest.mark.run_paths(moonraker_conf="invalid_config.conf")
|
||||
def test_invalid_config(path_args: Dict[str, pathlib.Path]):
|
||||
evtloop = EventLoop()
|
||||
args = {
|
||||
'config_file': str(path_args['moonraker.conf']),
|
||||
'log_file': "",
|
||||
'software_version': "moonraker-pytest"
|
||||
}
|
||||
with pytest.raises(ConfigError):
|
||||
Server(args, None, evtloop)
|
||||
|
||||
def test_config_and_log_warnings(path_args: Dict[str, pathlib.Path]):
|
||||
evtloop = EventLoop()
|
||||
args = {
|
||||
'config_file': str(path_args['moonraker.conf']),
|
||||
'log_file': "",
|
||||
'software_version': "moonraker-pytest",
|
||||
'log_warning': "Log Warning Test",
|
||||
'config_warning': "Config Warning Test"
|
||||
}
|
||||
expected = ["Log Warning Test", "Config Warning Test"]
|
||||
server = Server(args, None, evtloop)
|
||||
assert server.warnings == expected
|
||||
|
||||
@pytest.mark.run_paths(moonraker_conf="unparsed_server.conf")
|
||||
@pytest.mark.asyncio
|
||||
async def test_unparsed_config_items(full_server: Server):
|
||||
expected_warnings = [
|
||||
"Unparsed config section [machine unparsed] detected.",
|
||||
"Unparsed config option 'unknown_option: True' detected "
|
||||
"in section [server]."]
|
||||
warn_cnt = 0
|
||||
for warn in full_server.warnings:
|
||||
for expected in expected_warnings:
|
||||
if warn.startswith(expected):
|
||||
warn_cnt += 1
|
||||
assert warn_cnt == 2
|
||||
|
||||
@pytest.mark.run_paths(moonraker_log="moonraker.log")
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_logger(base_server: Server,
|
||||
path_args: Dict[str, pathlib.Path]):
|
||||
log_path = path_args.get("moonraker.log", None)
|
||||
assert log_path is not None and log_path.exists()
|
||||
|
||||
def test_signal_handler(base_server: Server,
|
||||
event_loop: asyncio.AbstractEventLoop):
|
||||
base_server._handle_term_signal()
|
||||
event_loop.run_forever()
|
||||
assert base_server.exit_reason == "terminate"
|
||||
|
||||
class TestInstantiation:
|
||||
def test_running(self, base_server: Server):
|
||||
assert base_server.is_running() is False
|
||||
|
||||
def test_app_args(self,
|
||||
path_args: Dict[str, pathlib.Path],
|
||||
base_server: Server):
|
||||
args = {
|
||||
'config_file': str(path_args['moonraker.conf']),
|
||||
'log_file': str(path_args.get("moonlog", "")),
|
||||
'software_version': "moonraker-pytest"
|
||||
}
|
||||
assert base_server.get_app_args() == args
|
||||
|
||||
def test_api_version(self, base_server: Server):
|
||||
ver = base_server.get_api_version()
|
||||
assert ver == API_VERSION
|
||||
|
||||
def test_pending_tasks(self, base_server: Server):
|
||||
loop = base_server.get_event_loop().aioloop
|
||||
assert len(asyncio.all_tasks(loop)) == 0
|
||||
|
||||
def test_klippy_info(self, base_server: Server):
|
||||
assert base_server.get_klippy_info() == {}
|
||||
|
||||
def test_klippy_state(self, base_server: Server):
|
||||
assert base_server.get_klippy_state() == "disconnected"
|
||||
|
||||
def test_host_info(self, base_server: Server):
|
||||
hinfo = {
|
||||
'hostname': socket.gethostname(),
|
||||
'address': "0.0.0.0",
|
||||
'port': 7010,
|
||||
'ssl_port': 7011
|
||||
}
|
||||
assert base_server.get_host_info() == hinfo
|
||||
|
||||
def test_klippy_connection(self, base_server: Server):
|
||||
assert base_server.klippy_connection.is_connected() is False
|
||||
|
||||
def test_components(self, base_server: Server):
|
||||
key_list = sorted(list(base_server.components.keys()))
|
||||
assert key_list == [
|
||||
"application",
|
||||
"internal_transport",
|
||||
"klippy_connection",
|
||||
"websockets",
|
||||
]
|
||||
|
||||
def test_endpoint_registered(self, base_server: Server):
|
||||
app = base_server.moonraker_app
|
||||
assert "/server/info" in app.api_cache
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notification(self, base_server: Server):
|
||||
base_server.register_notification("test:test_event")
|
||||
fut = base_server.event_loop.create_future()
|
||||
wsm = base_server.lookup_component("websockets")
|
||||
wsm.websockets[1] = MockWebsocket(fut)
|
||||
base_server.send_event("test:test_event", "test")
|
||||
ret = await fut
|
||||
expected = {
|
||||
'jsonrpc': "2.0",
|
||||
'method': "notify_test_event",
|
||||
'params': ["test"]
|
||||
}
|
||||
assert expected == ret
|
||||
|
||||
class TestLoadComponent:
|
||||
def test_load_component_fail(self, base_server: Server):
|
||||
with pytest.raises(ServerError):
|
||||
base_server.load_component(
|
||||
base_server.config, "invalid_component")
|
||||
|
||||
def test_failed_component_set(self, base_server: Server):
|
||||
assert "invalid_component" in base_server.failed_components
|
||||
|
||||
def test_load_component_fail_with_default(self, base_server: Server):
|
||||
comp = base_server.load_component(
|
||||
base_server.config, "invalid_component", None)
|
||||
assert comp is None
|
||||
|
||||
def test_lookup_failed(self, base_server: Server):
|
||||
with pytest.raises(ServerError):
|
||||
base_server.lookup_component("invalid_component")
|
||||
|
||||
def test_lookup_failed_with_default(self, base_server: Server):
|
||||
comp = base_server.lookup_component("invalid_component", None)
|
||||
assert comp is None
|
||||
|
||||
def test_load_component(self, base_server: Server):
|
||||
comp = base_server.load_component(base_server.config, "klippy_apis")
|
||||
assert isinstance(comp, KlippyAPI)
|
||||
|
||||
def test_lookup_component(self, base_server: Server):
|
||||
comp = base_server.lookup_component('klippy_apis')
|
||||
assert isinstance(comp, KlippyAPI)
|
||||
|
||||
def test_component_attr(self, base_server: Server):
|
||||
key_list = sorted(list(base_server.components.keys()))
|
||||
assert key_list == [
|
||||
"application",
|
||||
"internal_transport",
|
||||
"klippy_apis",
|
||||
"klippy_connection",
|
||||
"websockets",
|
||||
]
|
||||
|
||||
class TestCoreServer:
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def core_server(self, base_server: Server) -> AsyncIterator[Server]:
|
||||
base_server.load_components()
|
||||
yield base_server
|
||||
await base_server._stop_server("terminate")
|
||||
|
||||
def test_running(self, core_server: Server):
|
||||
assert core_server.is_running() is False
|
||||
|
||||
def test_http_servers(self, core_server: Server):
|
||||
app = core_server.lookup_component("application")
|
||||
assert (
|
||||
app.http_server is None and
|
||||
app.secure_server is None
|
||||
)
|
||||
|
||||
def test_warnings(self, core_server: Server):
|
||||
assert len(core_server.warnings) == 0
|
||||
|
||||
def test_failed_components(self, core_server: Server):
|
||||
assert len(core_server.failed_components) == 0
|
||||
|
||||
def test_lookup_components(self, core_server: Server):
|
||||
comps = []
|
||||
for comp_name in CORE_COMPONENTS:
|
||||
comps.append(core_server.lookup_component(comp_name, None))
|
||||
assert None not in comps
|
||||
|
||||
def test_pending_tasks(self, core_server: Server):
|
||||
loop = core_server.get_event_loop().aioloop
|
||||
assert len(asyncio.all_tasks(loop)) == 0
|
||||
|
||||
def test_register_component_fail(self, core_server: Server):
|
||||
with pytest.raises(ServerError):
|
||||
core_server.register_component("machine", object())
|
||||
|
||||
def test_register_remote_method(self, core_server: Server):
|
||||
core_server.register_remote_method("moonraker_test", lambda: None)
|
||||
kconn = core_server.klippy_connection
|
||||
assert "moonraker_test" in kconn.remote_methods
|
||||
|
||||
def test_register_method_exists(self, core_server: Server):
|
||||
with pytest.raises(ServerError):
|
||||
core_server.register_remote_method(
|
||||
"shutdown_machine", lambda: None)
|
||||
|
||||
class TestServerInit:
|
||||
def test_running(self, full_server: Server):
|
||||
assert full_server.is_running() is False
|
||||
|
||||
def test_http_servers(self, full_server: Server):
|
||||
app = full_server.lookup_component("application")
|
||||
assert (
|
||||
app.http_server is None and
|
||||
app.secure_server is None
|
||||
)
|
||||
|
||||
def test_warnings(self, full_server: Server):
|
||||
assert len(full_server.warnings) == 0
|
||||
|
||||
def test_failed_components(self, full_server: Server):
|
||||
assert len(full_server.failed_components) == 0
|
||||
|
||||
def test_lookup_components(self, full_server: Server):
|
||||
comps = []
|
||||
for comp_name in CORE_COMPONENTS:
|
||||
comps.append(full_server.lookup_component(comp_name, None))
|
||||
assert None not in comps
|
||||
|
||||
def test_config_backup(self,
|
||||
full_server: Server,
|
||||
path_args: Dict[str, pathlib.Path]):
|
||||
cfg = path_args["config_path"].joinpath(".moonraker.conf.bkp")
|
||||
assert cfg.is_file()
|
||||
|
||||
class TestServerStart:
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def server(self, full_server: Server) -> Server:
|
||||
await full_server.start_server(connect_to_klippy=False)
|
||||
return full_server
|
||||
|
||||
def test_running(self, server: Server):
|
||||
assert server.is_running() is True
|
||||
|
||||
def test_http_servers(self, server: Server):
|
||||
app = server.lookup_component("application")
|
||||
assert (
|
||||
app.http_server is not None and
|
||||
app.secure_server is None
|
||||
)
|
||||
|
||||
@pytest.mark.run_paths(moonraker_conf="base_server_ssl.conf")
|
||||
class TestSecureServerStart:
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def server(self, full_server: Server) -> Server:
|
||||
await full_server.start_server(connect_to_klippy=False)
|
||||
return full_server
|
||||
|
||||
def test_running(self, server: Server):
|
||||
assert server.is_running() is True
|
||||
|
||||
def test_http_servers(self, server: Server):
|
||||
app = server.lookup_component("application")
|
||||
assert (
|
||||
app.http_server is not None and
|
||||
app.secure_server is not None
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_component_init_error(base_server: Server):
|
||||
base_server.register_component("testcomp", MockComponent(err_init=True))
|
||||
await base_server.server_init(False)
|
||||
assert "testcomp" in base_server.failed_components
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_component_exit_error(base_server: Server,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
base_server.register_component("testcomp", MockComponent(err_exit=True))
|
||||
await base_server._stop_server("terminate")
|
||||
expected = "Error executing 'on_exit()' for component: testcomp"
|
||||
assert expected in caplog.messages
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_component_close_error(base_server: Server,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
base_server.register_component("testcomp", MockComponent(err_close=True))
|
||||
await base_server._stop_server("terminate")
|
||||
expected = "Error executing 'close()' for component: testcomp"
|
||||
assert expected in caplog.messages
|
||||
|
||||
def test_register_event(base_server: Server):
|
||||
def test_func():
|
||||
pass
|
||||
base_server.register_event_handler("test:my_test", test_func)
|
||||
assert base_server.events["test:my_test"] == [test_func]
|
||||
|
||||
def test_register_async_event(base_server: Server):
|
||||
async def test_func():
|
||||
pass
|
||||
base_server.register_event_handler("test:my_test", test_func)
|
||||
assert base_server.events["test:my_test"] == [test_func]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_event(full_server: Server):
|
||||
evtloop = full_server.get_event_loop()
|
||||
fut = evtloop.create_future()
|
||||
|
||||
def test_func(arg):
|
||||
fut.set_result(arg)
|
||||
full_server.register_event_handler("test:my_test", test_func)
|
||||
full_server.send_event("test:my_test", "test")
|
||||
result = await fut
|
||||
assert result == "test"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_async_event(full_server: Server):
|
||||
evtloop = full_server.get_event_loop()
|
||||
fut = evtloop.create_future()
|
||||
|
||||
async def test_func(arg):
|
||||
fut.set_result(arg)
|
||||
full_server.register_event_handler("test:my_test", test_func)
|
||||
full_server.send_event("test:my_test", "test")
|
||||
result = await fut
|
||||
assert result == "test"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_remote_method_running(full_server: Server):
|
||||
await full_server.start_server(connect_to_klippy=False)
|
||||
with pytest.raises(ServerError):
|
||||
full_server.register_remote_method(
|
||||
"moonraker_test", lambda: None)
|
||||
|
||||
@pytest.mark.usefixtures("event_loop")
|
||||
def test_main(path_args: Dict[str, pathlib.Path],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
tries = [1]
|
||||
|
||||
def mock_init(self: Server):
|
||||
reason = "terminate"
|
||||
if tries:
|
||||
reason = "restart"
|
||||
tries.pop(0)
|
||||
self.event_loop.delay_callback(.01, self._stop_server, reason)
|
||||
cfg_path = path_args["moonraker.conf"]
|
||||
args = MockArgs("", True, str(cfg_path))
|
||||
monkeypatch.setattr(Server, "server_init", mock_init)
|
||||
code: Optional[int] = None
|
||||
try:
|
||||
servermain(args)
|
||||
except SystemExit as e:
|
||||
code = e.code
|
||||
assert (
|
||||
code == 0 and
|
||||
"Attempting Server Restart..." in caplog.messages and
|
||||
"Server Shutdown" == caplog.messages[-1]
|
||||
)
|
||||
|
||||
@pytest.mark.run_paths(moonraker_conf="invalid_config.conf")
|
||||
def test_main_config_error(path_args: Dict[str, pathlib.Path],
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
cfg_path = path_args["moonraker.conf"]
|
||||
args = MockArgs("", True, str(cfg_path))
|
||||
try:
|
||||
servermain(args)
|
||||
except SystemExit as e:
|
||||
code = e.code
|
||||
assert code == 1 and "Server Config Error" in caplog.messages
|
||||
|
||||
@pytest.mark.run_paths(moonraker_conf="invalid_config.conf",
|
||||
moonraker_bkp=".moonraker.conf.bkp")
|
||||
@pytest.mark.usefixtures("event_loop")
|
||||
def test_main_restore_config(path_args: Dict[str, pathlib.Path],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture):
|
||||
def mock_init(self: Server):
|
||||
reason = "terminate"
|
||||
self.event_loop.delay_callback(.01, self._stop_server, reason)
|
||||
|
||||
cfg_path = path_args["moonraker.conf"]
|
||||
args = MockArgs("", True, str(cfg_path))
|
||||
monkeypatch.setattr(Server, "server_init", mock_init)
|
||||
code: Optional[int] = None
|
||||
try:
|
||||
servermain(args)
|
||||
except SystemExit as e:
|
||||
code = e.code
|
||||
assert (
|
||||
code == 0 and
|
||||
"Loaded server from most recent working configuration:" in caplog.text
|
||||
)
|
||||
|
||||
class TestEndpoints:
|
||||
@pytest_asyncio.fixture(scope="class")
|
||||
async def server(self, full_server: Server):
|
||||
await full_server.start_server()
|
||||
yield full_server
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_server_info(self,
|
||||
server: Server,
|
||||
http_client: HttpClient):
|
||||
ret = await http_client.get("/server/info")
|
||||
comps = list(server.components.keys())
|
||||
expected = {
|
||||
'klippy_connected': False,
|
||||
'klippy_state': "disconnected",
|
||||
'components': comps,
|
||||
'failed_components': [],
|
||||
'registered_directories': ["config", "logs"],
|
||||
'warnings': [],
|
||||
'websocket_count': 0,
|
||||
'moonraker_version': "moonraker-pytest",
|
||||
'missing_klippy_requirements': [],
|
||||
'api_version': list(API_VERSION),
|
||||
'api_version_string': ".".join(str(v) for v in API_VERSION)
|
||||
}
|
||||
assert ret["result"] == expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_server_config(self,
|
||||
server: Server,
|
||||
http_client: HttpClient):
|
||||
cfg = server.config.get_parsed_config()
|
||||
ret = await http_client.get("/server/config")
|
||||
assert ret["result"]["config"] == cfg
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_server_info(self,
|
||||
server: Server,
|
||||
websocket_client: WebsocketClient):
|
||||
ret = await websocket_client.request("server.info")
|
||||
comps = list(server.components.keys())
|
||||
expected = {
|
||||
'klippy_connected': False,
|
||||
'klippy_state': "disconnected",
|
||||
'components': comps,
|
||||
'failed_components': [],
|
||||
'registered_directories': ["config", "logs"],
|
||||
'warnings': [],
|
||||
'websocket_count': 1,
|
||||
'moonraker_version': "moonraker-pytest",
|
||||
'missing_klippy_requirements': [],
|
||||
'api_version': list(API_VERSION),
|
||||
'api_version_string': ".".join(str(v) for v in API_VERSION)
|
||||
}
|
||||
assert ret == expected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_server_config(self,
|
||||
server: Server,
|
||||
websocket_client: WebsocketClient):
|
||||
cfg = server.config.get_parsed_config()
|
||||
ret = await websocket_client.request("server.config")
|
||||
assert ret["config"] == cfg
|
||||
|
||||
def test_server_restart(base_server: Server,
|
||||
http_client: HttpClient,
|
||||
event_loop: asyncio.AbstractEventLoop):
|
||||
result = {}
|
||||
|
||||
async def do_restart():
|
||||
base_server.load_components()
|
||||
await base_server.start_server()
|
||||
ret = await http_client.post("/server/restart")
|
||||
result.update(ret)
|
||||
event_loop.create_task(do_restart())
|
||||
event_loop.run_forever()
|
||||
assert result["result"] == "ok" and base_server.exit_reason == "restart"
|
||||
|
||||
@pytest.mark.no_ws_connect
|
||||
def test_websocket_restart(base_server: Server,
|
||||
websocket_client: WebsocketClient,
|
||||
event_loop: asyncio.AbstractEventLoop):
|
||||
result = {}
|
||||
|
||||
async def do_restart():
|
||||
base_server.load_components()
|
||||
await base_server.start_server()
|
||||
await websocket_client.connect()
|
||||
ret = await websocket_client.request("server.restart")
|
||||
result["result"] = ret
|
||||
event_loop.create_task(do_restart())
|
||||
event_loop.run_forever()
|
||||
assert result["result"] == "ok" and base_server.exit_reason == "restart"
|
||||
|
||||
|
||||
# TODO:
|
||||
# test invalid cert, key (probably should do that in test_app.py)
|
||||
Reference in New Issue
Block a user