You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Common Lisp中用fiveam测试CLI参数解析的最佳实践及实现咨询

问题

我正在使用fiveam测试包,想要测试由main.lisp脚本处理的CLI参数解析逻辑,但负责解析参数的main函数不接收任何参数。因此我有以下疑问:

  1. 在该场景下,测试命令行参数解析的最佳实践是什么?是在测试函数运行时修改命令行参数,还是修改main.lisp中的main函数使其接收CLI参数作为入参?
  2. 若选定方案,具体该如何实现?
  3. 如何模拟传入"--interface wlo1"参数,来测试main-missing-arg-p中的main函数?

main.lisp

(in-package :cl-user)
(defpackage kafar
  (:use :cl)
  (:export main))

(in-package :kafar)
;;; CLI Parser

(opts:define-opts
  (:name :port
   :description "port on the loopback interface on which the proxy server shall run. Recommended: 1080"
   :short #\p
   :long "port"
   :required t
   :arg-parser #'parse-integer
  )
  (:name :interface
   :description "name of the interface kafar shall listen on"
   :short #\i
   :required t
   :long "interface"
   :arg-parser #'identity)
  (:name :help
   :description "print this help text"
   :short #\h
   :long "help"))

(defun unknown-option (condition)
  ...)

(defun missing-arg (condition)
  ...)

(defmacro when-option ((options opt) &body body)
  `...)

(defun main ()
(multiple-value-bind (options free-args)
    (handler-case
        (handler-bind ((opts:unknown-option #'unknown-option))
          (opts:get-opts))
      (opts:missing-arg (condition)
        (format t "fatal: option ~s needs an argument!~%"
                (opts:option condition))
        (uiop:quit))
      (opts:arg-parser-failed (condition)
        (format t "fatal: cannot parse ~s as argument of ~s.~%"
                (opts:raw-arg condition)
                (opts:option condition))
        (uiop:quit))
      ...
    (when-option (options :port)
    (let ((interfacei (getf options :interface))
          (porti (getf options :port)))
          (kafar/proxy:proxy-server porti interfacei))
    (uiop:quit)
    )))

test-main.lisp

(in-package :kafar/tests)

;;; Testing Proxy server
(def-suite* parse-suite
    :description "Test parsing of CLI arguments"
    :in kafar-suite)

;; test the stdout without arguments
(test main-help
    ; call main function without aguments
    (print (unix-opts:argv)) ; => ("sbcl")
    (let ((output (with-output-to-string (*error-output*) (kafar:main))))
      (is (string= "warning: missing required options: \"--port\", \"--interface\"" output))))

;; test stderr missing port argument
(test main-missing-arg-p
  ; define interface argument, not define port argument
  ;Potentially modify (unix-opts:argv) (sb-ext:*posix-argv*) such that they would be equal to ("sbcl" "--interface 80")
  ; call main function (handling CLI argument parsing)
  )

测试执行命令

sbcl --non-interactive --eval "(progn (ql:quickload '(fiveam usocket nibbles unix-opts trivial-coverage)) (asdf:load-asd (merge-pathnames \"kafar.asd\" (uiop:getcwd))) (asdf:test-system 'kafar/coverage))"

解答

1. 最佳实践选择

优先修改main函数,让它接收CLI参数作为入参,这是更合理的方案:

  • 解耦参数来源与解析逻辑,让解析逻辑可以独立测试,不用依赖全局命令行环境
  • 代码复用性更强,后续如果需要从配置文件或其他渠道传入参数,能直接调用解析逻辑
  • 避免修改全局变量带来的副作用,比如测试之间的参数污染

如果暂时不想改动原代码,也可以通过修改全局命令行参数变量来测试,但这只是临时方案,长期来看重构main函数更优。

2. 具体实现方案

方案一:重构main函数(推荐)

把参数解析逻辑抽成独立函数,让main函数支持接收自定义参数:

(in-package :kafar)

;; 新增独立的参数解析函数,接收参数数组
(defun parse-cli-args (args)
  (handler-case
      (handler-bind ((opts:unknown-option #'unknown-option))
        (opts:get-opts args)) ; 传入自定义参数,替代默认命令行参数
    (opts:missing-arg (condition)
      (format t "fatal: option ~s needs an argument!~%"
              (opts:option condition))
      (uiop:quit))
    (opts:arg-parser-failed (condition)
      (format t "fatal: cannot parse ~s as argument of ~s.~%"
              (opts:raw-arg condition)
              (opts:option condition))
      (uiop:quit))
    ;; 保留原有其他异常处理逻辑
    ...))

;; 修改原main函数,默认用命令行参数,也支持传入自定义参数
(defun main (&optional args)
  (multiple-value-bind (options free-args)
      (parse-cli-args (or args (opts:argv))) ; 优先使用传入的参数,否则用默认命令行参数
    (when-option (options :port)
      (let ((interfacei (getf options :interface))
            (porti (getf options :port)))
        (kafar/proxy:proxy-server porti interfacei))
      (uiop:quit))))

这样测试时直接给main函数传模拟参数即可,无需修改全局变量。

方案二:修改全局命令行参数(临时方案)

如果不想改动原代码,可以修改unix-opts的全局参数或CL实现的全局变量(比如SBCL的sb-ext:*posix-argv*),但要注意测试前后恢复原参数,避免影响其他测试。

3. 模拟传入"--interface wlo1"参数的测试实现

基于重构后的main函数(推荐)

直接给kafar:main传入模拟参数,同时捕获uiop:quit避免测试进程退出:

(test main-missing-arg-p
  (let ((output (with-output-to-string (*standard-output*)
                  (handler-case
                      (kafar:main '("kafar" "--interface" "wlo1")) ; 第一个元素是程序名,后面是参数
                    (uiop:quit () nil))))) ; 捕获退出信号,防止测试中断
    (is (string= "warning: missing required options: \"--port\"" output))))

基于修改全局变量的临时方案

测试前保存原参数,测试后恢复:

(test main-missing-arg-p
  (let ((original-argv (unix-opts:argv))) ; 保存原参数
    (unwind-protect
         (progn
           (setf (unix-opts:argv) '("sbcl" "--interface" "wlo1")) ; 设置模拟参数
           (let ((output (with-output-to-string (*standard-output*)
                           (handler-case
                               (kafar:main)
                             (uiop:quit () nil)))))
             (is (string= "warning: missing required options: \"--port\"" output))))
      (setf (unix-opts:argv) original-argv)))) ; 恢复原参数,避免影响其他测试

内容的提问来源于stack exchange,提问作者Polo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 09:17:05