【模拟LeetCode项目】04 - 项目模块详解

1. http服务器模块

1.1 模块解析

该模块分为三部分:

  1. Get请求1:获取所有试题列表 请求,并将 完整试题列表 响应给客户浏览器
  2. Get请求2:获取单个试题信息 请求,并将 选定的试题信息 响应给客户浏览器
  3. Post请求:请求 返回编译运行结果,并将 编译运行结果 响应给客户浏览器

1.2 模块代码

oj_server.cpp

/*
 *  http 模块
 */
#include <stdio.h>
#include <iostream>
#include <jsoncpp/json/json.h>

#include "oj_model.hpp"
#include "httplib.h"
#include "oj_view.hpp"
#include "compile.hpp"

using namespace httplib;

int main()
{
	/* 
		1. 初始化httplib库的server对象
	*/
  Server svr;
  ojModel model;
  
  	/* 
		2. 提供三个接口,分别处理三个请求
	*/
    
	// 2.1 接口一:获取整个试题列表 
  svr.Get("/all_questions",[&model](const Request& req, Response& resp){
     // 1.返回试题列表
     std::vector<Question> questions;
     model.GetAllQuestion(&questions);
	 
     // 2.将试题列表渲染至html页面上去
     std::string html;
     OJView::DrawAllQuestions(questions, &html);
     resp.set_content(html, "text/html");	//设置响应体
     });

  // 2.2 接口二:获取单个试题
  //  如何表示浏览器想要获取的是哪一个试题?
  //  使用正则表达式解决点击题目后自动跳转至对应的URL
  //  \d表示数字0-9
  //  \d+表示多位数字
  svr.Get(R"(/question/(\d+))",[&model](const Request& req, Response& resp){
      // 1.获取url当中关于试题的数字 & 获取单个试题的信息
      // matches[1]返回的是题目id,并不是一个string
      //std::cout << req.matches[1] << std::endl;
      Question ques;
      // 通过试题数字(id)进而获取试题信息
      model.GetOneQuestion(req.matches[1].str(),&ques); 
      
      // 2.渲染模板的html文件
      std::string html;
      OJView::DrawOneQuestion(ques, &html);  
      
      resp.set_content(html, "text/html");	//设置响应体
      });

  // 2.3 接口三:解释运行
  // 目前还没有区分到底是提交的是哪一个试题
      // 1.获取试题编号
      // 2.通过 试题编号 进而获取 题目内容 填充到ques中
      // 3.用户点击submit后从浏览器获取代码,将代码进行decode -> code
      // 4.将代码与tail.cpp合并:code + tail.cpp -> src文件 
      // 5.编译模块进行编译:
      //  编译src文件的技术:子进程创建,使用子进程程序替换成为"g++"来编译源码文件
      //    成功:
      //    失败:
      //  运行变异后的文件技术:子进程创建,使用子进程程序替换成为编译出来的可执行程序
      //    成功:
      //    失败:
  svr.Post(R"(/compile/(\d+))",[&model](const Request& req, Response& resp){
      // 1.获取题目id ---> ques_id 
      std::string ques_id = req.matches[1].str();
      
      // 2.获取题目内容 ---> ques 
      Question ques;
      model.GetOneQuestion(ques_id,&ques); 

      // 3.获取代码内容 ---> code 
      /*  
      以下注释中的代码容错率较低,如果代码中用户代码中出现=,就无法处理 
      std::vector<std::string> vec;
      StringUtil::Split(UrlUtil::UrlDecode(req.body), "=", &vec);
      std::string code = vec[1]; 
      */
      std::unordered_map<std::string, std::string> body_kv;
      UrlUtil::PraseBody(req.body, &body_kv);
      
      // 4.获取完整源文件code + tail.cpp ---> src
      std::string src = body_kv["code"] + ques.tail_cpp_;

      /*
       * 构造JSON对象
       */ 
      Json::Value req_json;
      req_json["code"] = src;
      req_json["stdin"] = "";
      //std::cout << req_json["code"].asString() << std::endl;
      
      // 获取的返回结果都在resp_json中
      Json::Value resp_json;
      Compiler::CompileAndRun(req_json, &resp_json);
      std::string case_result = resp_json["stdout"].asString();
      std::string reason = resp_json["reason"].asString();
      
      std::string html;	//用于返回的响应内容
      OJView::DawCaseResult(case_result, reason, &html);	// 将case_result和reason填充入html中

      resp.set_content(html, "text/html");	//设置响应体
      });

  // LOG(INFO, "17878") << ":" << std::endl;
  svr.set_base_dir("./www");
  svr.listen("0.0.0.0",18989);
  return 0;
}

2. 试题模块

2.1 模块解析

image-20210306093140377

  • oj_config.cfg:记录了所有试题的重要信息,可根据该文件能让找到对应试题

  • 试题的所有信息都被存储在文件中

2.2 模块代码

oj_model.hpp

/*
 *  试题模块
 */
#pragma once 
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>
#include "tools.hpp"


//一道试题需要哪些属性来描述?
struct Question
{
    std::string id_;    // 题目ID
    std::string title_;//题目名称
    std::string star_;//题目的难易程度
    std::string path_;//题目的路径

    std::string desc_;  //题目的描述
    std::string header_cpp_; //题目预定义的头
    std::string tail_cpp_; //题目的尾,包含测试用例以及调用逻辑
};

// 从磁盘中加载题目,给用户提供接口查询
class ojModel
{
    private:
    std::unordered_map<std::string, Question> ques_map_;  //题目ID和内容
    public:
    ojModel()
    {
        //从配置文件中获取试题信息
        load("./oj_data/oj_config.cfg"); 
    }
    ~ojModel()
    {

    }
    //从文件中获取试题信息
    bool load(std::string filename)
    {
        std::ifstream file(filename.c_str());
        if(!file.is_open())
        {
            std::cout << "config file open failed!" << std::endl;
            return false;
        }
        // 1.打开文件成功的情况
        //  1.1 从文件中获取每一行信息
        //    1.1.1 对于每一行信息,还需要 获取 题号、题目名称、题目难易程度、题目路径
        //    1.1.2 保存在Question结构体中
        // 2.把多个question组织在map中

        std::string line;
        while(std::getline(file, line))
        {
            //使用Boost库中的split函数:按照空格进行切割
            std::vector<std::string> vec;
            StringUtil::Split(line, "\t", &vec);
            // is_any_of:支持多个字符作为分隔符
            // token_compress_off:是否将多个分割字符看做是一个
            //boost::split(vec, line, boost::is_any_of(" "), boost::token_compress_off);
			
            Question ques;
            ques.id_ = vec[0];
            ques.title_ = vec[1];
            ques.star_ = vec[2];
            ques.path_ = vec[3];
            std::string dir = vec[3];
            FileUtil::ReadFile(dir + "/desc.txt",&ques.desc_);
            FileUtil::ReadFile(dir + "/header.cpp",&ques.header_cpp_);
            FileUtil::ReadFile(dir + "/tail.cpp",&ques.tail_cpp_);

            ques_map_[ques.id_] = ques;
        }
        file.close();
        return true;
    }
    // 提供给上层调用者一个获取所有试题的接口
    bool GetAllQuestion(std::vector<Question>* questions)
    {
        // 1.遍历无序的map,将试题信息返回给调用者
        // 对于每一个试题,都是一个Question结构体对象
        for(const auto& kv : ques_map_)
        {
            questions->push_back(kv.second);  // 此时,questions中保存的便是 所有的试题信息
        }
        // questions中试题的顺序并不是按顺序保存的,所以需要 调整顺序
        // 对试题进行 排序
        sort(questions->begin(),questions->end(),[](const Question& l, const Question& r){
            //升序:比较Question中的题目编号:将string->int
            return std::stoi(l.id_) < std::stoi(r.id_);
        });
    }
    // 提供给上层调用者一个获取单个试题的接口
    // id:待查找题目的id
    // ques:是输出参数,将查到的结果返回给调用者
    bool GetOneQuestion(const std::string& id, Question* ques)
    {
        auto it = ques_map_.find(id);
        if(it == ques_map_.end())
        {
            return false;
        }
        *ques = it->second;
        return true;
    }
};

3. 编译运行模块

3.1 模块解析

  • 将 从网页中获得的代码 和 头文件 和 测试文件 组装成一个 源文件
  • ,产生的源文件的命名方式为tmp_时间戳_变量值.cpp
    • 时间戳是用于区分不同时间创建出来的文件
    • 变量值是用于区分 高并发情况 下产生的 时间戳相同 的情况下 区分文件
  • 使用进程程序替换对源文件进行编译和运行,对运行时间和资源限制以防止死循环等
  • 根据编译与运行结果,设置json对象,通过KV形式将错误码(K)理由(V)返回给post请求的回调函数中

compile.hpp

/*
 *  编译运行模块
 */
#pragma once 
#include <atomic>
#include <iostream>
#include <string>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <jsoncpp/json/json.h>
#include "tools.hpp"

enum errorno
{
    OK = 0,
    PRAM_ERROR,   //参数错误
    INTERNAL_ERROR, //内部错误
    COMPILE_ERROR,  //编译错误
    RUN_ERROR //运行错误
};

class Compiler{
    public:
    /* 参数解释:
     * Json::Value Req 请求的json
     *    { "code":"xxx", "stdin":"xxx" }
     *  Json::Value* Resp 出参,返回给调用者
     *    { "errorno":"xx", "reason":"xxx" }
     */ 
    static void CompileAndRun(Json::Value Req, Json::Value* Resp)
    {
        // 1.参数是否错误
        if(Req["code"].empty())
        {
            (*Resp)["errorno"] = PRAM_ERROR;
            (*Resp)["reason"] = "Pram error";
            return;
        }
        // 2.将代码写到文件里面 文件命名规则:tmp_时间戳_变量值.cpp
        //    时间戳:用于区分不同时间创建出来的文件
        //    变量值:用于区分 高并发情况 下产生的 时间戳相同 的情况下 区分文件
        std::string code = Req["code"].asString();
        std::string file_nameheader = WriteTmpFile(code); 
        if(file_nameheader == "") //写文件失败
        {
            (*Resp)["errorno"] = INTERNAL_ERROR;
            (*Resp)["reason"] = "write file failed";
            return ;
        }
        // 3.编译
        if(!Compile(file_nameheader))
        {
            (*Resp)["errorno"] = COMPILE_ERROR;
            std::string reason;
            FileUtil::ReadFile(CompileErrorPath(file_nameheader).c_str(),&reason);
            (*Resp)["reason"] = reason;
            return ;
        }

        // 4.运行
        // -1 -2   >0
        int ret = Run(file_nameheader);
        if(ret != 0)
        {
            (*Resp)["errorno"] = RUN_ERROR;
            std::string reason = "program exit by sig:" + std::to_string(ret);
            (*Resp)["reason"] = reason;
            return ;
        }

        // 5.构造响应
        (*Resp)["errorno"] = OK;
        (*Resp)["reason"] = "Compile and Run OK";

        std::string stdout_str;
        FileUtil::ReadFile(stdoutPath(file_nameheader).c_str(), &stdout_str);
        (*Resp)["stdout"] = stdout_str;

        std::string stderr_str; 
        FileUtil::ReadFile(stderrPath(file_nameheader).c_str(), &stderr_str);
        (*Resp)["stderr"] = stderr_str;

        // 6.删除临时文件
        clean(file_nameheader); 

        return ;
    }

    private:
    static void clean(const std::string& file_name)
    {
        unlink(SrcPath(file_name).c_str());
        unlink(CompileErrorPath(file_name).c_str());
        unlink(stdoutPath(file_name).c_str());
        unlink(stderrPath(file_name).c_str());
        unlink(ExePath(file_name).c_str());
    }

    static int Run(const std::string& file_name)
    {
        // 1.创建子进程
        int pid = fork();
        // 2.父进程进行进程等待,子进程进行进程程序替换
        if(pid < 0)
        {
            return -1;
        }
        else if(pid > 0)
        {
            int status;
            waitpid(pid,&status,0);
            return status & 0x7f;
        }
        else 
        {
            // 注册一个定时器,防止用户提交的代码中含有死循环等长时间运行
            alarm(1);
            // 限制进程使用的资源
            struct rlimit rlim;
            rlim.rlim_cur = 30000 * 1024; // 3wk
            rlim.rlim_max = RLIM_INFINITY;
            setrlimit(RLIMIT_AS, &rlim);

            int stdout_fd = open(stdoutPath(file_name).c_str(), O_CREAT | O_WRONLY, 0666);
            if(stdout_fd < 0)
            {
                return -2;
            }
            dup2(stdout_fd,1);

            int stderr_fd = open(stderrPath(file_name).c_str(), O_CREAT | O_WRONLY, 0666);
            if(stderr_fd < 0)
            {
                return -2;
            }
            dup2(stderr_fd, 2);

            execl(ExePath(file_name).c_str(), ExePath(file_name).c_str(), NULL);
            exit(0);
        }
        return 0;
    }

    static bool Compile(const std::string file_name)
    {
        // 1.创建子进程 
        int pid = fork();
        // 2.父进程进行进程等待,子进程进行进程程序替换
        if(pid > 0)
        {
            waitpid(pid,NULL,0);
        }
        else if(pid == 0)
        {
            int fd = open(CompileErrorPath(file_name).c_str(), O_CREAT | O_WRONLY, 0666);
            if(fd < 0)
            {
                return false;
            }
            dup2(fd,2);// 将标准错误重定向为fd,标准错误的输出,将会被输出在文件当中

            // 进行进程程序替换成g++ SrcPath(file_name) -o ExePath(file_name) -std=c++11
            execlp("g++","g++",SrcPath(file_name).c_str(),"-o",ExePath(file_name).c_str(),"-std=c++11","-D","CompileOnline",NULL);

            close(fd);
            exit(0);
        }
        else 
        {
            return false;
        }
        // 如果替换失败了,就要返回false,判断失败的标准:是否产生了可执行文件
        // 使用stat函数查看可执行文件属性
        struct stat st;
        int ret = stat(ExePath(file_name).c_str(), &st);
        if(ret < 0)
        {
            return false;
        }
        return true;
    }


    static std::string stdoutPath(const std::string& filename)
    {
        return "./tmp_file/" + filename + "_stdout";
    }
    static std::string stderrPath(const std::string& filename)
    {
        return "./tmp_file/" + filename + "_stderr";
    }


    // 获取编译错误文件路径
    static std::string CompileErrorPath(const std::string& filename)
    {
        return "./tmp_file/" + filename + "_compil_err_info";
    }
    // 编译好的文件路径
    static std::string ExePath(const std::string& filename)
    {
        return "./tmp_file/" + filename + "_executable";
    }

    // 获取源文件路径和文件名 
    static std::string SrcPath(const std::string& filename)
    {
        return "./tmp_file/" + filename + ".cpp";
    }

    static std::string WriteTmpFile(const std::string& code)
    {
        // 1.组织文件名称,区分源码文件,以及后面生成的可执行文件
        static std::atomic_uint id(0);
        std::string tmp_filename = "tmp_" + std::to_string(TimeUtil::GetTimeStampMs()) + "_" + std::to_string(id);
        id++;
        // 2.将code写入文件中
        FileUtil::WriteFile(SrcPath(tmp_filename), code);
        return tmp_filename;
    }
};

4. 工具类模块

4.1 模块解析

工具类OJView:使用谷歌模板分离技术(ctemplate)将预定义好的html源码进行填充

  1. 将用户请求的 试题列表 渲染至响应界面上
  2. 将用户请求的 单个试题信息 渲染至响应界面上
  3. 用户请求的 代码运行结果 渲染至响应界面上

类FileUtil:根据文件名来读取文件或者写入文件

类StringUtil:用于字符串分割

类UrlUtil:用于对URL中Key1=Value1&Key2=Value2的字符串进行解码

类TimeUtil:获取时间戳

全局函数log:用于输出日志

4.2 模块代码

oj_view.hpp

/*
 *  工具类OJView:用于渲染HTML响应界面
 */
#pragma once 
#include <iostream>
#include <vector>
#include "ctemplate/template.h"
#include "oj_model.hpp"
// 该文件用于填充模板

class OJView
{
  public:
	/*
		DrawAllQuestions:渲染 试题列表 至响应页面上
	*/
    static void DrawAllQuestions(std::vector<struct Question>& questions, std::string* html)
    {
      // 1.创建template字典
      ctemplate::TemplateDictionary dict("all_questions");
      // 2.遍历vector,每一个试题构造成一个字典
      for(auto& ques : questions)
      {
        ctemplate::TemplateDictionary* sub_dict = dict.AddSectionDictionary("question");
        sub_dict->SetValue("id", ques.id_);
        sub_dict->SetValue("id",ques.id_);
        sub_dict->SetValue("title",ques.title_);
        sub_dict->SetValue("star",ques.star_);
      }
      // 3.填充
      ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/all_questions.html", ctemplate::DO_NOT_STRIP);
      // 4.渲染
      tl->Expand(html, &dict);
    }
    
	/*
		DrawOneQuestion:渲染 用户选中的单个试题信息 至响应页面上
	*/
    static void DrawOneQuestion(const Question& ques, std::string* html)
    {
      ctemplate::TemplateDictionary dict("question");
      dict.SetValue("id", ques.id_);
      dict.SetValue("title", ques.title_);
      dict.SetValue("star", ques.star_);
      dict.SetValue("desc", ques.desc_);
      dict.SetValue("id", ques.id_);
      dict.SetValue("code", ques.header_cpp_);
      // 填充
      ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/question.html", ctemplate::DO_NOT_STRIP);
      // 渲染
      tl->Expand(html, &dict);
    }
	
	/*
		DawCaseResult:渲染 提交运行返回信息 至响应页面上
	*/
    static void DawCaseResult(const std::string& q_result, const std::string& reason, std::string* html)
    {
      ctemplate::TemplateDictionary dict("question");
      dict.SetValue("compile_result", reason);
      dict.SetValue("case_result", q_result);
      ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/case_result.html", ctemplate::DO_NOT_STRIP);
      // 渲染
      tl->Expand(html, &dict);
    }

};//class end

tools.hpp

#pragma once 
#include <string>
#include <iostream>
#include <fstream>
#include <sys/time.h>
#include <string>
#include <vector>
#include <boost/algorithm/string.hpp>
#include <unordered_map>
class FileUtil
{
    public:
    // 读文件接口 + 写文件接口

    //filename:文件名称
    //content:读取到的内容,输出型参数
    static bool ReadFile(const std::string& file_name, std::string* content)
    {
        content->clear();
        std::ifstream file(file_name.c_str());
        if(!file.is_open())
        {
            return false;
        }
        std::string line;
        while(std::getline(file, line))
        {
            (*content) += line + "\n";
        }
        file.close();
        return true;
    }

    static bool WriteFile(const std::string& filename, const std::string data)
    {
        std::ofstream file(filename.c_str());
        if(!file.is_open())
            return false;
        file.write(data.c_str(), data.size());
        file.close();
        return true;
    }
};

class StringUtil{
    public:
    static void Split(const std::string& input, const std::string split_char, std::vector<std::string>* output)
    {
        boost::split(*output, input, boost::is_any_of(split_char), boost::token_compress_off);
    }
};

class UrlUtil
{
    public:

    //body从httplib.h当中的request类的成员变量获得
    //  body:
    //     key=value&key1=value1   ===> 并且是进行过编码
    //  1.先切割
    //  2.在对切割之后的结果进行转码
    static void PraseBody(const std::string& body, std::unordered_map<std::string, std::string>* body_kv)
    {
        std::vector<std::string> kv_vec;
        StringUtil::Split(body, "&", &kv_vec);

        for(const auto& t : kv_vec)
        {
            std::vector<std::string> sig_kv;
            StringUtil::Split(t, "=", &sig_kv);

            if(sig_kv.size() != 2)
            {
                continue;
            }

            //在保存的时候, 针对value在进行转码
            (*body_kv)[sig_kv[0]] = UrlDecode(sig_kv[1]);
        }
    }

    static unsigned char ToHex(unsigned char x)   
    {   
        return  x > 9 ? x + 55 : x + 48;   
    }  

    static unsigned char FromHex(unsigned char x)   
    {   
        unsigned char y;  
        if (x >= 'A' && x <= 'Z') y = x - 'A' + 10;  
        else if (x >= 'a' && x <= 'z') y = x - 'a' + 10;  
        else if (x >= '0' && x <= '9') y = x - '0';  
        else assert(0);  
        return y;  
    }  

    static std::string UrlEncode(const std::string& str)  
    {  
        std::string strTemp = "";  
        size_t length = str.length();  
        for (size_t i = 0; i < length; i++)  
        {  
            if (isalnum((unsigned char)str[i]) ||   
                (str[i] == '-') ||  
                (str[i] == '_') ||   
                (str[i] == '.') ||   
                (str[i] == '~'))  
                strTemp += str[i];  
            else if (str[i] == ' ')  
                strTemp += "+";  
            else  
            {  
                strTemp += '%';  
                strTemp += ToHex((unsigned char)str[i] >> 4);  
                strTemp += ToHex((unsigned char)str[i] % 16);  
            }  
        }  
        return strTemp;  
    }  

    static std::string UrlDecode(const std::string& str)  
    {  
        std::string strTemp = "";  
        size_t length = str.length();  
        for (size_t i = 0; i < length; i++)  
        {  
            if (str[i] == '+') strTemp += ' ';  
            else if (str[i] == '%')  
            {  
                assert(i + 2 < length);  
                unsigned char high = FromHex((unsigned char)str[++i]);  
                unsigned char low = FromHex((unsigned char)str[++i]);  
                strTemp += high*16 + low;  
            }  
            else strTemp += str[i];  
        }  
        return strTemp;  
    } 
};

//获取时间戳
class TimeUtil
{
    public:
    static int64_t GetTimeStampMs()
    {
        struct timeval tv;
        gettimeofday(&tv, NULL);
        return tv.tv_sec + tv.tv_usec / 1000;
    }

    // 获取年月日时分秒的时间戳
    static void GetTimeStamp(std::string* TimeStamp)
    {
        time_t st;
        time(&st);

        struct tm* t = localtime(&st);
        char buf[30] = {'\0'};
        snprintf(buf,sizeof(buf) - 1, "%04d-%02d-%02d %02d:%02d:%02d",t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min,  t->tm_sec);
        TimeStamp->assign(buf,strlen(buf));
    }
};

// 使用枚举值拿到对应字符串
enum logLevel
{
    INFO = 0,
    WARNING,
    ERROR,
    FATAL,
    DEBUG
};
const char* level[] = 
{
    "INFO",
    "WARNING",
    "ERROR",
    "FATAL",
    "DEBUG"
};

/*
 * lev : 日志等级
 * file:文件名称
 * line:行号
 * logmsg:想要记录的日志内容 
 */
std::ostream& log(logLevel lev, const char* file,int line, const std::string& logmsg)
{
    std::string level_info = level[lev];
    std::string TimeStamp;
    TimeUtil::GetTimeStamp(&TimeStamp);
    std::cout << "[" << TimeStamp << " " << level_info << " " << file << ":" << line << "]" << " " << logmsg;
    return std::cout;
}
#define LOG(lev, msg) log(lev, __FILE__, __LINE__, msg)

4.3 待填充html模板文件源码

HTML源码中{}中的字符串是可以使用ctemplate中的TemplateDictionary中的字段来进行替换

all_questions.html

<html>
    <head>
        <meta http-equiv="content-type" content="text/html;charset=utf-8">
    </head>
    <body>
        {{#question}}
        <div>
            <a href="/question/{{id}}">
                {{id}}.{{title}}({{star}})
            </a>
        </div>
        {{/question}}
    </body>
</html>

question.html

<html>
    <head>
        <meta http-equiv="content-type" content="text/html;charset=utf-8">
    </head>
    <body>
        <div>
            {{id}}.{{title}}({{star}})
        </div>
        <div>
            <pre>
            {{desc}}
            </pre>
        </div>
        <div>
            <form action="/compile/{{id}}" method="post">
                <textarea name="code" rows=30 cols=100>{{code}}</textarea>
                <br>
                <textarea name="stdin" rows=30 cols=100>hello</textarea>
                <input type="submit" value="Submit">
            </form>
        </div>
    </body>
</html>

case_result.html

<html>
    <head>
        <meta http-equiv="content-type" content="text/html;charset=utf-8">
    </head>
    <body>
        <div><pre>error_no : {{errno}}</pre></div>
        <div><pre>{{compile_result}}</pre></div>
        <div><pre>{{case_result}}</pre></div>
    </body>
</html>

5. 项目模块间调用逻辑结构

在这里插入图片描述

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值