【问题标题】:"coverage run app.py" inside a docker container not creating .coverage file?docker容器内的“coverage run app.py”没有创建.coverage文件?
【发布时间】:2018-02-08 18:24:57
【问题描述】:

系统定义:

2012 macbook pro 运行 Sierra, Docker 版本:17.12.0-ce-mac49 (21995)

问题:

我在容器中使用“coverage run”运行我的 python 应用程序时遇到问题。我将尝试在下面解释我在做什么......希望它是有道理的。尝试在此处提供尽可能多的信息,说明我正在尝试做的事情以及我已经完成的事情。

基本上我的情况是这样的。我正在启动一个基于 python 的(金字塔)网络服务。它只是一个基于 HTTP 的 API。没有什么超级花哨的。当我对它运行邮递员测试时,我想获得它的覆盖率,所以我通过执行“coverage run /path/path/path/app.py”来启动它,并且效果很好。我在要分析的源代码上创建了一个 .coveragerc,将其归零,一切都很好,在容器之外。尝试在我的这个应用的容器化版本中进行覆盖是另一回事。

供参考.. 这是我的 Dockerfile,其中包含用######### 标记掩盖的秘密内容

FROM ################:latest

# Add files
ADD . / /path/

# Set up Env
ENV GRAPHITE_SERVER=localhost
ENV APP_USER=root
ENV APP_HOME=/path
ENV APP_LOGS=$APP_HOME/logs
ENV PYTHONPATH=#################:$PYTHONPATH
ARG requested_environment=stage1
ARG coverage
ARG log_monitor
ENV COVERAGE=$coverage
ENV LOG_MONITOR=$log_monitor
ENV ENVIRONMENT=$requested_environment
ENV PATH=$APP_HOME/######:$PATH

RUN mkdir -p $APP_LOGS
RUN pip install -r $APP_HOME/requirements.txt
RUN cd $APP_HOME/external-libs/mysql-connector/mysql-connector-python-2.1.2/; python setup.py install

RUN ln -fs /usr/bin/python /usr/local/bin/python

EXPOSE 1234

# start app
ENTRYPOINT $COVERAGE $APP_HOME/path/path/app.py $LOG_MONITOR

我的构建命令是

docker build . --build-arg requested_environment=stage1 --build-arg log_monitor=noMonitor --build-arg coverage="coverage run" -t stage1-latest

我想在容器内做与在容器外做同样的事情......意思是......我想在容器内运行应用程序并覆盖。所以你会认为它就像正常构建容器一样简单,入口点通常是

ENTRYPOINT /path/path/path/app.py
changed to
ENTRYPOINT coverage run /path/path/path/app.py

但显然不是。

如果我使用 ENTRYPOINT 或 CMD 启动应用程序来构建容器.. 就像这些.. 是的,我已经尝试了所有这些..

ENTRYPOINT coverage run /path/path/path/app.py
ENTRYPOINT bash -c "coverage run /path/path/path/app.py"
ENTRYPOINT ["bash' "-c","coverage run /path/path/path/app.py"]
ENTRYPOINT ["coverage","run","/path/path/path/app.py"]
CMD coverage run /path/path/path/app.py
CMD ["bash' "-c","coverage run /path/path/path/app.py"]
or
CMD ["coverage","run","/path/path/path/app.py"]

然后像这样启动容器

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest

甚至在 dockerfile 中的 CMD 条目的情况下也是如此(因为它们是默认的,我用这个命令覆盖它们)

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest coverage run /path/path/path/app.py

我的应用程序启动非常好..没有问题..但是

没有创建 .coverage 文件!在容器的文件系统中我没有看到 .coverage 文件。

好的..尝试差异方法

把 dockerfile 改成有

ENTRYPOINT bash
or
CMD bash

重建它...

然后运行容器 docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest

这在 docker 容器中给了我一个提示。

[root@ac25fb69bb2e /]#

然后从另一个窗口执行此操作

docker exec -it stage1-latest-container-instance coverage run /path/path/path/app.py

或其他类似 docker exec 的方法

docker exec -it stage1-latest-container-instance bash -c "coverage run /path/path/path/app.py"

还是!不会创建 .coverage 文件。

好的.. 采用差异化方法 我什至创建了一个名为 runapp.sh 的文件并将以下行放入其中

#!/bin/bash
cd /path/path/path && coverage run app.py 

然后用

启动容器

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest /path/runapp.sh

没有 .coverage 文件 然后我尝试只用

运行容器

docker run --name stage1-latest-container-instance -itp 1234:1234 stage1-latest bash

然后从另一个窗口做

docker exec -it stage1-latest-container-instance /path/runapp.sh

仍然没有 .container 文件

好吧..最后的手段..

进入我在交互模式 (-it) 的 dockerfile 中仅使用“CMD bash”运行“docker run”后得到的提示。然后手动启动我的应用程序。

[root@ac25fb69bb2e /]# coverage run /path/path/path/app.py

而且令人惊讶的是.. .coverage 文件的创建没有问题....

什么鬼! 我无法弄清楚这一点..我不知道发生了什么...但是从容器外部对容器执行的命令与在内部的单个外壳内手动执行的命令之间似乎完全断开了容器是否可以创建文件。

请有人告诉我我在这里做错了什么。

我能看到的唯一下一步是在我的应用程序内部使用覆盖模块,并为 KeyboardInterrupt 添加一个异常处理程序,以在应用程序被终止时保存覆盖数据。但这很糟糕。

【问题讨论】:

    标签: python docker containers dockerfile coverage.py


    【解决方案1】:

    对于任何像我一样从谷歌跌跌撞撞的人......

    为了扩展 Wolfium 的答案,这是由于 docker 在覆盖将数据写入其数据文件之前杀死了 python 进程(或者更准确地说,python 无法优雅地解释 docker 的默认 SIGTERM 信号并且在几秒钟后它得到一个SIGKILL)。解决此问题的最快方法是:

    • 确保 python 进程能够接收相关信号。如果您在 shell 入口点脚本中运行它,您可能需要在前面加上 exec,例如exec coverage -m swagger_server
    • 默认你的图像的停止信号为SIGINT,这是大多数python处理程序实现的:将STOPSIGNAL SIGINT行添加到你的Dockerfile。

    稍微好一点的解决方案是确保您的 python 处理程序可以适当地响应SIGTERM

    【讨论】:

      【解决方案2】:

      这并不是我最初想要的,但这是我最终必须做的才能让这一切正常工作。将其发布为我自己问题的答案,以防其他人发现自己处于同样的困境并想知道我是如何解决这个问题的。

      我最终不得不在我的 app.py 脚本中使用覆盖 api。我一生都无法弄清楚为什么在运行容器时将其作为入口点或 cmd 传递时使用“覆盖运行”不起作用。仍然不确定为什么。如果有人知道为什么会分享,我会很高兴。

      在我的 app.py 中,我做了以下操作

      当它最初启动时,我会检查是否设置了运行时标志,我称之为“withcoverage”。

      if withcoverage:
          print "starting code coverage engine"
          import coverage
          os.system("mkdir %s/tmpcoveragedata" % os.environ['HEYTHERE_HOME'])
          coveragedatafile = ".coverage-"+str(int(time.time()))
          cov = coverage.Coverage(data_file=os.environ['APP_HOME']+"/tmpcoveragedata/"+coveragedatafile,config_file=os.environ['APP_HOME']+"/.coveragerc")
          cov.start()
      

      然后我继续照常启动服务器。 但是我必须添加一个 try: 块来捕获 KeyboardInterrupt (SIGINT) 并在发生这种情况时退出之前保存覆盖范围。像这样:

      try:
          # start the server etc.. etc..
          .
          .
          .
      except KeyboardInterrupt as e:
          if withcoverage:
              print "saving coverage stats"
              save_coverage(cov)
              os.system("bash -c 'cd %s/tmpcoveragedata && rm -f .coverage ; cd %s/tmpcoveragedata && ln -s %s .coverage" % (os.environ['APP_HOME'],os.environ['APP_HOME'],coveragedatafile))
              os.system("bash -c 'cd %s/tmpcoveragedata/ && coverage report > %s/tmpcoveragedata/coverage-report-%s.txt'" % (os.environ['APP_HOME'],os.environ['APP_HOME'],coveragedatafile))
          print "exiting program"
          exit()
      

      当我现在构建我的容器时,我将主源 repo 中的一个卷挂载到容器内的 tmpcoveragedata 目录。然后,当容器接收到 SIGINT 并退出时生成的覆盖率文件和报告会保存到该临时文件夹中,该文件夹会映射到运行容器的机器上的适当覆盖率数据存储。

      这是我的 docker run 命令

      docker run --name stage1-latest-container-instance -d -v /app_home/coveragedata:/app_home/tmpcoveragedata -p 1234:1234 stage1-latest
      

      现在正在运行..它将运行具有覆盖范围的应用程序..在退出时使用 SIGINT 保存覆盖范围。

      有一个警告。当停止容器一个简单的

      docker stop <container-name> 
      

      是不够的。我需要执行以下操作,以便应用程序在我的自动化停止容器时看到 KeyboardInterrupt。

      docker kill --signal=SIGINT stage1-latest-container-instance
      

      希望这可以帮助其他处理此问题的人。

      【讨论】:

        【解决方案3】:

        很棒的信息和细节,解释和展示解决方法。 它在正确的方向上帮助了我很多。

        根本原因是在 docker 中运行时,您的修补覆盖服务正在运行,并且覆盖作为 docker 中的入口点运行直到完成运行才会保存。

        所以,如果你能够像你一样打破它,那么对我来说也是如此。 只是我正在使用 Flask,我发现它使用的开发服务器捕获关键中断异常,我无法关闭覆盖范围内的服务服务器。 最后,我找到了一种方法来获取一条从内部向烧瓶开发服务器发出信号的路由,它就像你首先向我展示的那样工作。

        非常感谢分享...

        【讨论】:

          【解决方案4】:

          在@Andy-T 的回答之上,我发现 Python 本身(或者可能只有 Flask 开发服务器)没有正常关闭以响应 SIGTERM 信号,即使在 docker 之外也是如此。因此,通过为 SIGTERM 注册一个信号处理程序,我能够调用 sys.exit(),这使 coverage.py 有机会写出数据。

          这是我的 app.py 文件:

          import sys
          import signal
          from flask import Flask
          
          
          def exit_cleanly(signal_number, stackframe):
              sys.exit()
          
          
          signal.signal(signal.SIGTERM, exit_cleanly)
          
          app = Flask(__name__)
          
          
          @app.route("/")
          def hello():
              return "Hello World!"
          

          这里的重点:

          • 标准库中的signal 模块允许您在python 从操作系统接收到特定信号时将函数注册为“回调”。
          • signal.SIGTERM是linux和macos上kill命令默认发送的信号
          • sys.exit 不带参数允许 python 解释器关闭,执行所有正常的清理步骤——包括注册在退出时运行的 coverage.py 代码

          然后,在我的(macos 或 linux)终端中,我运行了这些命令来提供覆盖率数据没有 docker

          python3 -m venv venv
          . venv/bin/activate
          pip3 install Flask
          coverage run -m flask run &> /tmp/flask.log &
          curl http://localhost:5000/
          kill "$(ps -ef | grep 'coverage run' | grep -v grep | awk '{ print $2 }')"
          coverage report
          

          详情:

          • 第一两行完全是可选的,但建议将依赖项隔离到虚拟环境中(与 OP 无关)
          • coverage run -m flask run &amp;&gt; /tmp/flask.log &amp; 将 stderr/stdout 放入 /tmp 目录中的文件中,这样终端就不会变得混乱和不可读 (&amp;&gt; /tmp/flask.log)。它还将进程发送到后台(尾随&amp;
          • curl http://localhost:5000/ -- 这只是作为 Flask 应用程序的轻量级测试发出请求。您可以将其替换为您的实际测试运行(又名 py.test 或行为或其他)
          • kill "$(ps -ef | grep 'coverage run' | grep -v grep | awk '{ print $2 }')" -- 使用ps -ef 搜索正在运行的进程,然后过滤到仅包含coverage run 的进程,然后删除自身(grep -v 表示删除匹配行),然后找到具有PID 的第二个字段。最后,kill 向 PID 发送 SIGTERM。因此,这应该会导致 exit_cleanly() 回调触发并拆除烧瓶进程。
          • coverage report 只是将摘要打印到终端,以便您查看这是否有效

          为了在 docker 中运行它,我将所有这些打包到一个 bash 脚本中,并使其成为 docker 容器的入口点。这是一个存储库,它散列所有这些工作并与 CircleCI + Codecov 集成作为样板示例: https://github.com/brycefisher/coveragepy-flask

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-05-14
            • 2015-05-04
            • 2013-11-20
            • 2020-07-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-12-21
            相关资源
            最近更新 更多