51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 5840|回复: 4
打印 上一主题 下一主题

JCoverage使用

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2005-11-29 23:30:31 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
使用目的
JCoverage正如其名,主要用于代码的覆盖测试。通过在被测试类的二进制文件中添加相应的指令,再配合junit,jcoverage可以给出被测试类的代码覆盖率,并给出测试过程中哪些语句被执行,哪些没有。通过这些信息,我们可以了解测试代码的测试质量,以及被测试类中哪些语句需要特别的关注(即那些未被覆盖的语句)。

当前的版本是1.0.5,可以从www.jcoverage.com/downloads.html下载。在实际使用时,需要将jcoverage用到的类放到对应的classpath中。在这个版本中它所依赖的外部类以及版本:

- bcel    5.1     http://jakarta.apache.org/bcel/

- log4j   1.2.8    http://jakarta.apache.org/log4j/

- getopt  1.0.9    http://gnu.org/

- oro     2.0.7   http://jakarta.apache.org/oro/



用法
1.       jcoverage可以和ant配合使用,使这一过程自动化。基本用法:

-          编译被测试类和相应的被测试类的junit代码。

-          使用<instrument>,将jcoverage的指令加入到被测试类的二进制文件中。为了保证这一过程能够有效,请在编译被测试类时,设置<javac>的debug属性设为yes。

-          执行junit代码进行代码覆盖测试。这一步需要注意的是,需要将被注入指令的测试类要早于单元测试所需的测试类装入。为了确保这一点,需要使用<classpath>,使注入指令的测试类位于测试类之前。

-          使用<report>产生覆盖测试的报告。

2.       使用例子:

<!-- 采用debug模式编译被测试类 -->

<javac destdir="${dist.coverage.classes}" deprecation="on" debug="yes">

<src path="${src.code}"/>

    <classpath refid="classpath"/>

</javac>

<!-- 编译被测试类的junit代码 -->

<javac destdir="${dist.coverage.junit}" deprecation="on">

    <src path="${src.junit}"/>

    <classpath refid="classpath"/>

</javac>

<!-- 定义jcoverage任务 -->

<path id="jcoverage">

        <fileset dir="junit_lib">

               <include name="jcoverage.jar"/>

        </fileset>

    </path>

<taskdef classpathref="jcoverage" resource="tasks.properties"/>

<!-- 给被测试类二进制代码中注入jcoverage的指令 -->

<instrument todir="${dist.coverage.instrument}">

        <classpath refid="classpath"/>

    <!-- 忽略org.apache.common的应用 -->

    <ignore regex="org.apache.common.*"/>



    <fileset dir="${dist.coverage.classes} ">

        <include name="**/*.class"/>

    </fileset>

</instrument>

<!-- 启动junit,进行代码覆盖测试 -->

<junit printsummary="yes" haltonfailure="no" fork="yes">

    <classpath>

        <!-- 注意:被注入指令的类,位置在测试类之前。

这样确保它们早于测试类备加载,使指令起作用。 -->

        <pathelement location="${dist.coverage.instrument}"/>

        <path refid="classpath"/>

        <pathelement location="${dist. coverage.junit}"/>

    </classpath>



    <formatter type="xml"/>



        <batchtest todir="${doc.jcoverageReport}">

         <fileset dir="${src.junit}" includes="**/*Test.java" />

     </batchtest>

</junit>

<!-- 产生代码覆盖测试报告 -->

<report srcdir="${src.code}" destdir="${doc.jcoverageReport}">

3.       jcoverage提供的ant任务标签有:

-          <instrument>,在类的二进制文件中注入jcoverage指令。注意:一定是debug模式下编译的类。

-          <report>,产生jcoverage报告,默认格式是HTML。也可以产生XML格式方便应用客户自定义的xslt产生其他格式的报告。产生XML格式的报告:

<report srcdir="${src.code}" destdir="${doc.jcoverageReport}" format="xml">

-          <merge>,整合多个由jcoverage产生的输出文件。整个jcoverage实际的运作过程是:首先,jcoverage给测试类注入指令产生新类,被注入指令的类在junit运行过程中产生输出文件(后缀为ser),最后根据输出文件产生报告。如果有多个输出文件,jcoverage提供了<merge>来整合这些文件,最后,从整合文件中产生整体报告。用法:

<merge>

<fileset dir="${basedir}">

<include name="**/jcoverage.ser"/>

</fileset>

</merge>

-          <check>,检查测试是否达到设定的标准,它可以和<report>配合使用。使用例子:

例1  :

<check branch="95" line="95"/>,意思是分支覆盖率需要达到95%,行覆盖率达到95%。

例2   

<check branch="95" line="95">

    <regex pattern="xptoolkit.jcoverage.*" branch="85" line="95"/>

</check>

包名符合xptoolkit.jcoverage.*模式的分支覆盖率达到85%,行覆盖率达到95%其余的分支覆盖率需要达到95%,行覆盖率达到95%。

检查表
       在使用jcoverage时需要注意的问题:

-          被jcoverage注入指令的类必须是在debug模式下编译的类。建议将那些使用debug模式编译的类的存放与最终的产品的目录分开。

-          使用了<instrument>的类,与源类的目录也最好分开。

-          对于那些使用了AOP(如AspectJ)技术的项目,jcoverage不能很好的应用到那些已经添加了Aspect信息的类。

-          代码覆盖工具只是给出了指导性的意见,不要过分依赖和相信它的结果。
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏
回复

使用道具 举报

该用户从未签到

2#
 楼主| 发表于 2005-11-29 23:34:12 | 只看该作者

EasyMock使用

在实际编写unit test的过程中,很多情况下我们会需要使用其他的组件(如和数据库相关的测试)。当我们是使用第三方提供的组件时,这并不是太大的问题,因为我们可以假设他们是无错的。但是一旦这些组件是我们自己的组件时,问题就暴露出来了。
“单元测试出错了,但到底是谁的错。我的?他的?”,这种情况显然违反了测试的独立性原则。出现这种情况使得这个测试无法确切的指出是那个单元出现了问题,照成了排错的困难,而且也浪费了时间。同时,过分的将单元的测试代码依赖于其他单元,也照成了其他一些很现实的问题:

-          在所依赖的单元完成之前,无法顺利的编写单元测试。

-          产生环境的依赖性,如运行一个HttpServletRequest处理器的测试代码必须启动一个servlet容器。

所有这些问题,都可以使用Mock Object来解决。使用它的前提是,所依赖单元的接口必须定义清楚。而EasyMock正是为了这一目的而产生的。

使用目的
       通过模拟unit test所需要的组件,已达到隔离各个unit test的目的。目前的版本1.1,它所需要的环境是jdk1.3.1以上和junit3.8.1以上。

可以从http://sourceforge.net/projects/easymock/处下载。

用法
1.       EasyMock采用“记录-----回放”的工作模式,基本使用步骤:

-          创建Mock对象的控制对象Control。

-          从控制对象中获取所需要的Mock对象。

-          记录测试方法中所使用到的方法和返回值。

-          设置Control对象到“回放”模式。

-          进行测试。

-          在测试完毕后,确认Mock对象已经执行了刚才定义的所有操作。

2.       使用举例:假设需要测试的对象是RequestUtil,所需要测试的方法为getBoolean。此时我们需要模拟一个HttpServletRequest对象。

public void testGetStringHttpServletRequestString() {

public void testGetBoolean() {

   //创建Mock对象的控制器

   MockControl  control= MockControl.createControl( HttpServletRequest.class);

   //获取Mock对象

   HttpServletRequest  mock= (HttpServletRequest)control.getMock();

   //设置getBoolean中要使用的方法和返回值

control.expectAndReturn( mock.getParameter( "test"), null);

//设置控制器为replay模式

        control.replay();

        //执行测试

        assertNull( RequestUtil.getString( mock, "test"));

        //确认

        control.verify();

    }

}

通过EasyMock,执行这段测试代码完全不需要启动一个servlet容器,在命令行的方式下即可完成,非常的方便。

3.       记录需要使用的Mock的行为。在使用一个Mock对象之前,需要设置我们要用到的方法,以及每个方法的返回值。对于那些没有设置的方法,一旦调用(控制器处于replay模式),EasyMock就会抛出异常。记录一个方法,通常可以分成2步:首先,如同使用正常对象调用这个方法;然后,使用控制器的setReturnValue函数设置即可。在1.1中,提供了expectAndReturn函数,使得2步可以合而为一。主要的函数大致如下:

-          expectAndReturn,设置期望调用的函数,以及返回值

-          expectAndThrow,设置期望调用的函数,同时期望该次调用抛出异常

-          setReturnValue,设置上一次调用的返回值(如上次调用时,request.getparameter( “test”),此处设置request.getparameter( “test”)的返回值)

-          setThrowable,设置上次调用抛出的异常

在EasyMock中还可以设置调用所执行的次数,具体细节请参见对应的javaDoc。

4.       使用举例:

MockControl  control= MockControl.createControl( HttpServletRequest.class);

HttpServletRequest  mock= (HttpServletRequest)control.getMock();

mock.getParameter( "test");

//设置第一次调用request.getParameter的返回值

control.setReturnValue( null, 1);

//设置第二次调用request.getParameter的返回值

control.setReturnValue("this is a test", 1);

control.replay();

assertEquals( RequestUtil.getString( mock, "test", "haha"), "haha");

assertEquals( RequestUtil.getString( mock, "test"), "this is a test");

control.verify();

5.       在Mock对象使用结束后,务必调用控制器的verify函数,以确认Mock对象的方法得到了调用。

6.       调用方法的次序,使用Strict。有时,测试代码依赖于被依赖组件的方法的次序。如在测试与数据库相关代码时,测试代码很有可能是这样的次序:先打开数据库链接,执行操作,关闭链接。为了更好的测试这样的代码,可以使用MockControl.createStrictControl()来创建一个严格的Mock对象控制器,在其中,他会规定Mock出来的对象的调用次序。

以上是EasyMock的主要使用,至于其他的用法,请参见具体的文档。

检查表
       在此列出使用Mock对象来进行unit test需要注意的问题:

    - 当自己动手实现Mock对象时,不要在Mock对象中实现业务逻辑。
回复 支持 反对

使用道具 举报

该用户从未签到

3#
 楼主| 发表于 2005-11-29 23:34:53 | 只看该作者

StrutsTest使用

Struts是目前在web开发中广泛使用的几个框架之一,而StrutsTest正是专门负责测试strut应用程序的Mock测试框架。
使用目的
       StrtusTest是junit的扩展,使用它,不需要启动servlet容器就可以方便的测试struts应用程序(容器外测试)。它也是属于使用Mock对象测试,但是与EasyMock不同的是,EasyMock是提供了创建Mock对象的API,而StrutsTest则是专门负责测试Struts应用程序的Mock对象测试框架。除了容器外测试,StrutsTest也可以很方便的用容器内的测试。

       当前的版本是StrutsTest2.1.0,在这个版本中不支持Struts1.0。如果需要测试struts1.0的应用程序,对应的版本是StrutsTest2.0。可以从http://sourceforge.net/projects/strutstestcase/下载。

用法
1.       基本步骤:

-          书写struts action。

-          确定需要进行测试的方式:Mock对象则选用MockStrutsTestCase作为基类;Cactus方式则选用CactusStrutsTestCase;这两种方式以下的步骤都是一样的。

-          使用setUp和tearDown进行初始化,明确这两个函数的第一句话是调用super的方法。

-          设置要测试的struts action的路径,方便strutstest能找到web.xml和struts相关的配置文档。

-          设置action在strtus-config.xml中对应的path名称。

-          设置action需要使用的各个参数值到request对象中,包括action所使用的formbean的值。

-          执行action。

-          验证action的返回路径是否正确,即forward的名字。

-          验证action相关的其余部分。

2.       使用例子(采用Mock方式):

public class DeparmentDBActionTest extends MockStrutsTestCase {

    public DeparmentDBActionTest(String arg0) {

        super(arg0);

    }

public void setUp(){

        super.setUp();

//指明web应用的根

        File contextFile = new File("D:\\Projects\\fog\\implement\\web");

        setContextDirectory(contextFile);

    }

    protected void tearDown() throws Exception {

        super.tearDown();

    }

    public void testDoAdd() {

        //设置action的path

        setRequestPathInfo("/dpEdit");

        //准备action所需要的formbean的参数

        addRequestParameter("method", "doAdd");

        addRequestParameter("pageId", "1");

        addRequestParameter("dpname","测试部门");

        addRequestParameter("dptype","测试部门");

        addRequestParameter("yn","n");

        //执行action

        actionPerform();

        //验证返回的forward

        verifyForward("success");

    }

3.       主要函数和作用:

-          setContextDirectory,设置web应用的根

-          setRequestPathInfo,设置request的请求

-          addRequestParameter,将参数和对应的值加入request中

-          actionPerform,执行这个请求

-          verifyForward,验证forward的名字是否正确

-          verifyForwardPath,验证forward的path是否正确

-          verifyNoActionErrors,验证在action执行过程中没有ActionError产生

-          verifyActionErrors,验证在action执行过程中产生的ActionError集合的内容

具体详细的内容请参见对应的javaDoc。

4.       测试Tile。其基本过程与上面类似,不同之处在于验证:使用verifyTilesForward和verifyInputTilesForward。

5.       测试子模块。其基本过程也和上面一样,不同之处在于需要指定该模块所需要使用的struts-config.xml和调用方式:

-          首先,指定所需要的struts-config.xml:

setConfigFile("mymodule","/WEB-INF/struts-config-mymodule.xml");

-          其次,指明调用的路径:

setRequestPathInfo("/mymodule","/login.do");

检查表
       使用StrutsTest需要注意的地方:

-          在TestCase中可以访问request、session等对象,StrutsTest为我们提供了访问接口。

-          junit的assert函数和fail函数都可以在StrutsTest中使用。

-  对于文件上载,目前StrutsTest并没有什么特别好的方案。具体参见StrusTest的Faq。
回复 支持 反对

使用道具 举报

该用户从未签到

4#
 楼主| 发表于 2005-11-29 23:35:32 | 只看该作者

使用ant编译、运行junit、以及检查编码

胡键(james.hu@chinacodeline.com)

西安烁程软件有限公司

2004年1月

        在java开发中,ant为我们提供了一个非常强大的项目构建功能。日常开发中,最常用的功能是:编译、打包、运行junit testcase、检查编码规范。在此给出这些最常用功能的一个ant模板文件,与各位共享。呵呵

<?xml version="1.0" encoding="gb2312"?>
<project name="Test Project" default="all" basedir=".">

    <property name="src.code" value="src"/>
    <property name="src.junit" value="junit"/>
    <property name="lib.dir" value="lib"/>
    <property name="lib.jar" value="TestClass.jar"/>
   
    <!-- checkstyle configuration -->
    <property name="checkstyle.config" value="checkstyle.xml"/>
    <taskdef resource="checkstyletask.properties"
             classpath="${lib.dir}/checkstyle-all-3.1.jar"/>
   
   
    <!-- 输出文档 -->
    <property name="doc.dir" value="doc"/>
    <property name="doc.api" value="${doc.dir}/api"/>
    <property name="javadoc.package" value="com.*"/>
   
    <!-- 输出二进制文件 -->
    <property name="dist.root" value="dist"/>
    <property name="dist.proj" value="${dist.root}/proj"/>
    <property name="dist.classes" value="${dist.proj}/classes"/>
    <property name="dist.lib" value="${dist.proj}/lib"/>
    <property name="dist.junit" value="${dist.root}/junit"/>
   
    <!-- classpath -->
    <path id="classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar"/>
        </fileset>
    </path>
   
    <path id="proj.libs">
        <fileset dir="${dist.lib}">
            <include name="**/*.jar"/>
        </fileset>      
    </path>
   
    <target name="init">
        <mkdir dir="${doc.dir}"/>
        <mkdir dir="${dist.root}"/>
        <mkdir dir="${dist.proj}"/>
        <mkdir dir="${dist.lib}"/>
        <tstamp/>
        <echo message="${TSTAMP}"></echo>
    </target>
   
    <target name="all" depends="compilesrc, javadoc, checkstyle"/>
   
    <!-- 编译源文件 -->
    <target name="compilesrc" depends="init">
        <mkdir dir="${dist.classes}"/>
        
        <javac destdir="${dist.classes}" deprecation="on">
            <src path="${src.code}"/>
            <classpath refid="classpath"/>
        </javac>

        <jar jarfile="${dist.lib}/${lib.jar}" basedir="${dist.classes}">
            <include name="**/*.class"/>
        </jar>      
    </target>
   
    <!--  产生javadoc -->
    <target name="javadoc" depends="init">
        <mkdir dir="${doc.api}"/>
        
        <javadoc packagenames="${javadoc.package}" sourcepath="${src.code}"
             private="yes" defaultexcludes="yes" destdir="${doc.dir}/api">
            <classpath refid="classpath"/>
        </javadoc>
    </target>
   
    <!--  编译Junit文件 -->
    <target name="compilejunit" depends="compilesrc">
        <mkdir dir="${dist.junit}"/>
        
        <javac destdir="${dist.junit}" deprecation="on">
            <src path="${src.junit}"/>
            <classpath refid="classpath"/>
            <classpath refid="proj.libs"/>
        </javac>        
    </target>
   
    <!-- 运行checkstyle检查代码规范 -->
    <target name="checkstyle" depends="init">
        <checkstyle config="${checkstyle.config}">
            <fileset dir="${src.code}" includes="**/*.java"/>
            <formatter type="plain"/>
            <formatter type="xml" toFile="${dist.root}/checkstyle_errors.xml"/>
        </checkstyle>
    </target>
   
    <!--  运行junit  -->
    <target name="junit" depends="compilejunit">
        <junit printsummary="yes" haltonfailure="yes">
            <classpath>
                <path refid="classpath"/>
                <pathelement location="${dist.junit}"/>
            </classpath>
            
            <formatter type="plain"/>
        
            <!-- test name="com.TestClassTest" haltonfailure="no" outfile="result"/ -->
        
            <batchtest todir="${dist.junit}">
                <fileset dir="${dist.junit}" includes="**/Test*.class" />
            </batchtest>
        </junit>
    </target>
   
    <!-- 清除产生的类、junit相关类、文档 -->
    <target name="clean">
        <delete dir="${dist.classes}"/>
        <delete dir="${dist.junit}"/>
        <delete dir="${doc.api}"/>
    </target>
   
    <!-- 清除所有输出结果 -->
    <target name="cleanall" depends="clean">
        <delete dir="${doc.dir}"/>
        <delete dir="${dist.root}"/>
    </target>
</project>
回复 支持 反对

使用道具 举报

该用户从未签到

5#
 楼主| 发表于 2005-11-29 23:37:06 | 只看该作者

C#文档自动化

本文主要参考《inside C#》第15章的内容。

熟悉java的程序员都知道,在java中有一种“文档注释”。采用这种注释之后,使用相应的命令,我们就可以得到与代码相关的文档说明。如今,在.net的世界中C#也提供了相同的功能。如果结合相应的工具,它还可以为我们产生漂亮的WEB风格的文档。

文档自动化初步:
       在C#中文档注释对应的符号是:///。但光使用它还是不能为我们产生代码文档,还必须使用特殊的标记才行。这些标记实际上是XML标记,最常用的是< summary >。例如:

/// <summary>

/// A method with a string array param.

/// </summary>

public void Koo(string[] ss) {}

       但是,并不是所有使用文档注释和这些标记的地方编译器都会为我们生成文档,它还会看这些标记是否与一些代码结构相关联。例如:

/// <summary>

/// 不产生这行

/// </summary>

就不产生任何文档。这些代码结构必须是:class, struct, enum, method, property, field, indexer, delegate, 或event.

产生文档的命令
-          命令行:csc /doc: ….xml …..cs;

-          如使用VS.net,则:项目 -> 属性 -> 配置属性 -> 生成 -> 输出 -> xml文档文件(在此填写文件名和路径);

-          如要生成web注释:工具 ->生成注释 web......。

输出文档的类型描述符
字符
描述

N
Namespace

T
类型:class, interface, struct, enum, delegate

F
Field

P
Property (包括indexers or other indexed properties)

M
Method (包括constructors和operators)

E
Event

!
Error:编译器无法解析这个元素。


       一个输出的文件注释的示例:

<member name="F:SimpleXML.Class1.h">

    <summary>

    An enum field.

    </summary>

</member>

<member name="T:SimpleXML.Class1.hType">

    <summary>

    An enum type.

    </summary>

</member>

xml文档标记。
标记
描述

<c>
指示这行注释标识为Code

<code>
指示多行注释标识为Code

<example>
经常与<code>连用,用来给出如何使用某些成员的例子。

<exception>
指明一个成员会抛出哪些异常,经常与cref属性连用。

<include>
指明注释在哪些文件中,以及位置。

<list>
用来定义表头,经常与<item>连用

<newpara>
内部使用,如<remarks>或<returns>。让用户有机会给注释文本加入其他的结构。

<param>
说明参数的属性,编译器会检查参数的合法性。如果通不过,会在文档中产生!标识的警告。

<paramref>
类似<param>。

<permission>
标明用于成员的代码存取的安全性。

<remarks>
用于描述class或其它类型的描述性文字,不涉及具体细节(如果是这样,使用<summary>)。

<returns>
描述方法或函数的返回值。

<see>
指定一个链接。

<seealso>
指定要出现在See Also部分的文本。

<summary>
类型的描述性文字。它会被vs.net内置的IntelliSense使用并显示在对应的类型中。(即在vs.net中击键“.”出现的提示)

<value>
描述属性。


以上标记的属性。

标记
描述

cref
可用于任何标记来提供一个代码元素的参考。编译器将检查这个代码元素是否存在,如不存在则在文档中用!标识。

name
用于<param>或<paramref>


标记使用的例子
1.         <param>标记

/// <summary>

/// A method with a string array param.

/// </summary>

/// <param name="ss"></param>

public void Koo(string[] ss) {}



2.         <returns>标记

/// <summary>

/// A nonvoid method.

/// </summary>

/// <returns>The result of the operation.</returns>

public int Noo() { return 0; }



3.         <exception>标记和cref 属性:

/// <summary>

/// <exception cref="System.Exception">

/// Throws a FileIOException when...

/// </exception>

/// </summary>

public void Foo() {}



4.         <c>, <code>, 和<example>标记

/// <summary>

/// <c>Hoo</c> is a method in the <c>Class1</c> class.

/// </summary>

public void Hoo() {}



/// <summary>

/// The Joo method.

/// <example>This example shows how to use Joo:

/// <code>

/// <newpara/>

/// public static void Main()

/// {

///     Console.WriteLine(Class1.Joo());

/// }

/// <newpara/>

/// </code>

/// </example>

/// </summary>

public static int Joo() { return 0; }



5.         <include> 标记

语法:

<include file='filename' path='tagpath[@name="id"]' />



/// <include file='supporting.xml' path='MyDocs/MyMembers[@name="Class1"]/*' />

class Class1{

public static void Main() {}

}

supporting.xml

<MyDocs>

    <MyMembers name="Class1">

        <summary>

        The summary for this type.

        </summary>

    </MyMembers>

    <MyMembers name="Class2">

        <summary>

        Another type description.

        </summary>

    </MyMembers>

</MyDocs>



6.         <list>标记

语法:

<list type="bullet" │ "number" │ "table">

    <listheader>

        <term>term</term>

        <description>description</description>

    </listheader>

    <item>

        <term>term</term>

        <description>description</description>

    </item>

</list>



/// <remarks>Here is an example of a bulleted list:

/// <list type="bullet">

/// <item>

/// <description>Item 1.</description>

/// </item>

/// <item>

/// <description>Item 2.</description>

/// </item>

/// </list>

/// </remarks>

static void Main(string[] args) {}



扩充
       由于产生的文档是XML文件,使得我们对它的处理非常方便。如可以定义一个xls来让它产生符合我们需要的文档,如HTML、WORD等等。对于HTML,C#的编译器有内置的支持。我们可以使用标准的HTML标记来扩充它。但由于XML是well-formed的,因此对于一些没有</…>符号的HTML元素则必须作为一个XML有效元素使用。如<br>,对应为<br/>。
回复 支持 反对

使用道具 举报

本版积分规则

关闭

站长推荐上一条 /1 下一条

小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

GMT+8, 2024-5-6 23:16 , Processed in 0.070558 second(s), 26 queries .

Powered by Discuz! X3.2

© 2001-2024 Comsenz Inc.

快速回复 返回顶部 返回列表