【问题标题】:How to use pytest to simulate full reboot如何使用pytest模拟完全重启
【发布时间】:2023-03-29 14:08:01
【问题描述】:

如何测试我的程序对意外关闭的鲁棒性?

我的 Python 代码将在意外关闭的微控制器上运行。我想测试代码的每个部分意外重启并验证它是否正确处理。

尝试: 我尝试将代码放入它自己的进程中,然后提前终止它,但这不起作用,因为 MyClass 从命令行调用 7zip,即使在进程终止后也会继续:

import multiprocessing
import os

def MyClass(multiprocessing.Process):
   ...
   def run():
      os.system("7z a myfile.7z myfile")


process = MyClass()
process.start()
time.sleep(4)
print("terminating early")
process.terminate()
print("done")

我想要什么:

class TestMyClass(unittest.TestCase):
    def test_MyClass_continuity(self):
        myclass = MyClass().start()
        myclass.kill_everything()
        myclass = MyClass().start()
        self.assert_everything_worked_as_expected()

有没有简单的方法来做到这一点?如果没有,你如何设计可以在任何时候终止的健壮代码(例如测试状态机)?

类似问题(截至 21 年 10 月 26 日仍未得到答复):Simulating abnormal termination in pytest

非常感谢!

【问题讨论】:

  • 您的第一个代码示例有一些语法错误,但除此之外,不要将您要在 init 中运行的子进程中运行的代码放在主进程中过程。虽然正确的行为应该是阻止而不是后台运行,但您确定它在后台运行吗?
  • 谢谢,你是对的,现在更新了 - 代码应该在自己的进程中运行(在 run 函数中定义),而不是在 init.
  • 可以process groups在这里帮忙吗?
  • 微控制器或微处理器之类的树莓派?
  • idk 关于 pytest,但我会采取的方法是查看每个系统调用(主要是打开的文件/附加的硬件),并确保您处理它可能返回的每种类型的错误。目录不存在:创建它。未附加串行设备:等待附加事件。等等……

标签: python error-handling multiprocessing pytest microcontroller


【解决方案1】:

我认为这是数据持久性和一致性的问题。您还需要确保所有持久性(即写入磁盘)的数据也是一致的。

想象一下写入状态文件的某种数据。意外终止后应用程序将读取什么内容?新状态的一半和旧状态的一半?一半的新状态,其余的都是 0x00?

所以你的问题的答案 “你如何设计可以随时终止的健壮代码?” 是在处理持久数据时使用原子操作。大多数数据库在这个方向上提供了一些保证。对于处理本地文件,我个人通常使用重命名文件。这样我就可以写入一个临时文件而不用担心一致性,并且只有在完成后(确保刷新缓冲区!)因此一致我使用重命名的原子操作来使临时文件成为新的单点真理,因此也是持久的。如果在过程中的任何时候应用程序意外终止,持久数据将始终保持一致。它要么是以前的状态(以及临时文件中的一些垃圾),要么是新的状态,但两者之间没有。

无论您选择什么,请务必阅读有关原子性的文档以了解可能发生的情况。 IE。在正确的时间点中断的文件重命名可能看起来像创建硬链接。

请注意,仅仅杀死一个进程并不等同于切断电源,因为操作系统会继续运行并关闭文件、刷新缓冲区等。例如,在使用 SQLite 时,我很少看到“日志文件”而只是杀死应用程序,但是停电的时候经常看到。

【讨论】:

    【解决方案2】:

    您的逻辑启动一个封装在 MyClass 对象中的进程,该对象本身通过 os.system 调用生成一个新进程。

    当您终止 MyClass 进程时,您会终止父进程,但会使 7zip 进程作为孤儿运行。

    此外,process.terminate 方法向子进程发送SIGTERM 信号。子进程可以拦截所述信号并在终止之前执行一些清理例程。如果您想模拟没有机会清理的情况(断电),这并不理想。您很可能希望发送 SIGKILL 信号(在 Linux 上)。

    要杀死父子进程,需要寻址整个进程组。

    import os
    import time
    import signal
    import multiprocessing
    
    
    class MyClass(multiprocessing.Process):
        def run(self):
            # Ping localhost for a limited amount of time
            os.system("ping -c 12 127.0.0.1")
    
    
    process = MyClass()
    process.start()
    
    time.sleep(4)
    
    print("terminating early")
    
    # Send SIGKILL signal to the entire process group
    group_id = os.getpgid(process.pid)
    os.killpg(group_id, signal.SIGKILL)
    
    print("done")
    

    以上内容仅适用于 Unix 操作系统,不适用于 Windows 操作系统。

    对于 Windows,您需要使用 psutil 模块。

    import os
    import time
    import multiprocessing
    
    import psutil
    
    
    class MyClass(multiprocessing.Process):
        def run(self):
            # Ping localhost for a limited amount of time
            os.system("ping -c 12 127.0.0.1")
    
    
    def kill_process_group(pid):
        process = psutil.Process(pid)
        children = process.children(recursive=True)
    
        # First terminate all children
        for child in children:
            child.kill()
        psutil.wait_procs(children)
    
        # Then terminate the parent process
        process.kill()
        process.wait()
    
    
    process = MyClass()
    process.start()
    
    time.sleep(4)
    
    print("terminating early")
    
    kill_process_group(process.pid)
    
    print("done")
    

    【讨论】:

    • 非常感谢!
    猜你喜欢
    • 1970-01-01
    • 2020-12-20
    • 1970-01-01
    • 1970-01-01
    • 2012-01-17
    • 1970-01-01
    • 1970-01-01
    • 2019-12-30
    • 1970-01-01
    相关资源
    最近更新 更多