Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
xxadev
jenkins
提交
69de461c
J
jenkins
项目概览
xxadev
/
jenkins
与 Fork 源项目一致
从无法访问的项目Fork
通知
3
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
J
jenkins
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
69de461c
编写于
5月 07, 2013
作者:
O
olivergondza
浏览文件
操作
浏览文件
下载
差异文件
Merge pull request #584 from olivergondza/parametrized-filter
Use build parameters in combination filters
上级
8274ae73
b26835ff
变更
6
隐藏空白更改
内联
并排
Showing
6 changed file
with
436 addition
and
17 deletion
+436
-17
changelog.html
changelog.html
+3
-0
core/src/main/java/hudson/matrix/Combination.java
core/src/main/java/hudson/matrix/Combination.java
+9
-1
core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java
...ava/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java
+78
-14
core/src/main/java/hudson/matrix/MatrixProject.java
core/src/main/java/hudson/matrix/MatrixProject.java
+29
-2
core/src/main/java/hudson/model/Cause.java
core/src/main/java/hudson/model/Cause.java
+33
-0
core/src/test/java/hudson/matrix/CombinationFilterUsingBuildParamsTest.java
.../hudson/matrix/CombinationFilterUsingBuildParamsTest.java
+284
-0
未找到文件。
changelog.html
浏览文件 @
69de461c
...
...
@@ -78,6 +78,9 @@ Upcoming changes</a>
<li
class=
'major bug'
>
Properly find parent POMs when fingerprinting a Maven project.
(
<a
href=
"https://issues.jenkins-ci.org/browse/JENKINS-17775"
>
issue 17775
</a>
)
<li
class=
rfe
>
Allow the combination filter to accept parameter values.
(
<a
href=
"https://issues.jenkins-ci.org/browse/JENKINS-7285"
>
issue 7285
</a>
)
</ul>
</div>
<!--=TRUNK-END=-->
...
...
core/src/main/java/hudson/matrix/Combination.java
浏览文件 @
69de461c
...
...
@@ -111,10 +111,18 @@ public final class Combination extends TreeMap<String,String> implements Compara
* true.
*/
public
boolean
evalGroovyExpression
(
AxisList
axes
,
String
expression
)
{
return
evalGroovyExpression
(
axes
,
expression
,
new
Binding
());
}
/**
* @see #evalGroovyExpression(AxisList, String)
* @since 1.515
*/
public
boolean
evalGroovyExpression
(
AxisList
axes
,
String
expression
,
Binding
binding
)
{
if
(
Util
.
fixEmptyAndTrim
(
expression
)==
null
)
return
true
;
Binding
binding
=
new
Binding
();
for
(
Map
.
Entry
<
String
,
String
>
e
:
entrySet
())
binding
.
setVariable
(
e
.
getKey
(),
e
.
getValue
());
...
...
core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java
浏览文件 @
69de461c
package
hudson.matrix
;
import
groovy.lang.Binding
;
import
groovy.lang.GroovyRuntimeException
;
import
hudson.AbortException
;
import
hudson.Extension
;
import
hudson.Util
;
...
...
@@ -8,6 +10,7 @@ import hudson.matrix.MatrixBuild.MatrixBuildExecution;
import
hudson.matrix.listeners.MatrixBuildListener
;
import
hudson.model.BuildListener
;
import
hudson.model.Cause.UpstreamCause
;
import
hudson.model.ParameterValue
;
import
hudson.model.ParametersAction
;
import
hudson.model.Queue
;
import
hudson.model.ResourceController
;
...
...
@@ -113,27 +116,21 @@ public class DefaultMatrixExecutionStrategyImpl extends MatrixExecutionStrategy
@Override
public
Result
run
(
MatrixBuildExecution
execution
)
throws
InterruptedException
,
IOException
{
MatrixBuild
build
=
execution
.
getBuild
();
MatrixProject
p
=
execution
.
getProject
();
PrintStream
logger
=
execution
.
getListener
().
getLogger
();
Collection
<
MatrixConfiguration
>
touchStoneConfigurations
=
new
HashSet
<
MatrixConfiguration
>();
Collection
<
MatrixConfiguration
>
delayedConfigurations
=
new
HashSet
<
MatrixConfiguration
>();
for
(
MatrixConfiguration
c:
execution
.
getActiveConfigurations
())
{
if
(!
MatrixBuildListener
.
buildConfiguration
(
build
,
c
))
continue
;
// skip rebuild
if
(
touchStoneCombinationFilter
!=
null
&&
c
.
getCombination
().
evalGroovyExpression
(
p
.
getAxes
(),
getTouchStoneCombinationFilter
()))
{
touchStoneConfigurations
.
add
(
c
);
}
else
{
delayedConfigurations
.
add
(
c
);
}
}
filterConfigurations
(
execution
,
touchStoneConfigurations
,
delayedConfigurations
);
if
(
notifyStartBuild
(
execution
.
getAggregators
()))
return
Result
.
FAILURE
;
if
(
sorter
!=
null
)
{
touchStoneConfigurations
=
createTreeSet
(
touchStoneConfigurations
,
sorter
);
delayedConfigurations
=
createTreeSet
(
delayedConfigurations
,
sorter
);
delayedConfigurations
=
createTreeSet
(
delayedConfigurations
,
sorter
);
}
if
(!
runSequentially
)
...
...
@@ -148,7 +145,9 @@ public class DefaultMatrixExecutionStrategyImpl extends MatrixExecutionStrategy
notifyEndBuild
(
run
,
execution
.
getAggregators
());
r
=
r
.
combine
(
getResult
(
run
));
}
PrintStream
logger
=
execution
.
getListener
().
getLogger
();
if
(
touchStoneResultCondition
!=
null
&&
r
.
isWorseThan
(
touchStoneResultCondition
))
{
logger
.
printf
(
"Touchstone configurations resulted in %s, so aborting...%n"
,
r
);
return
r
;
...
...
@@ -170,6 +169,71 @@ public class DefaultMatrixExecutionStrategyImpl extends MatrixExecutionStrategy
return
r
;
}
private
void
filterConfigurations
(
final
MatrixBuildExecution
execution
,
final
Collection
<
MatrixConfiguration
>
touchStoneConfigurations
,
final
Collection
<
MatrixConfiguration
>
delayedConfigurations
)
throws
AbortException
{
final
MatrixBuild
build
=
execution
.
getBuild
();
final
String
combinationFilter
=
execution
.
getProject
().
getCombinationFilter
();
final
String
touchStoneFilter
=
getTouchStoneCombinationFilter
();
try
{
for
(
MatrixConfiguration
c:
execution
.
getActiveConfigurations
())
{
if
(!
MatrixBuildListener
.
buildConfiguration
(
build
,
c
))
continue
;
// skip rebuild
final
Combination
combination
=
c
.
getCombination
();
if
(
touchStoneFilter
!=
null
&&
satisfies
(
execution
,
combination
,
touchStoneFilter
))
{
touchStoneConfigurations
.
add
(
c
);
}
else
if
(
satisfies
(
execution
,
combination
,
combinationFilter
))
{
delayedConfigurations
.
add
(
c
);
}
}
}
catch
(
GroovyRuntimeException
ex
)
{
PrintStream
logger
=
execution
.
getListener
().
getLogger
();
logger
.
println
(
ex
.
getMessage
());
ex
.
printStackTrace
(
logger
);
throw
new
AbortException
(
"Failed executing combination filter"
);
}
}
private
boolean
satisfies
(
final
MatrixBuildExecution
execution
,
final
Combination
combination
,
final
String
filter
)
{
return
combination
.
evalGroovyExpression
(
execution
.
getProject
().
getAxes
(),
filter
,
getConfiguredBinding
(
execution
)
);
}
private
Binding
getConfiguredBinding
(
final
MatrixBuildExecution
execution
)
{
final
Binding
binding
=
new
Binding
();
final
ParametersAction
parameters
=
execution
.
getBuild
().
getAction
(
ParametersAction
.
class
);
if
(
parameters
==
null
)
return
binding
;
for
(
final
ParameterValue
pv:
parameters
)
{
if
(
pv
==
null
)
continue
;
final
String
name
=
pv
.
getName
();
final
String
value
=
pv
.
createVariableResolver
(
null
).
resolve
(
name
);;
binding
.
setVariable
(
name
,
value
);
}
return
binding
;
}
private
Result
getResult
(
@Nullable
MatrixRun
run
)
{
// null indicates that the run was cancelled before it even gets going
return
run
!=
null
?
run
.
getResult
()
:
Result
.
ABORTED
;
...
...
core/src/main/java/hudson/matrix/MatrixProject.java
浏览文件 @
69de461c
...
...
@@ -48,6 +48,8 @@ import hudson.model.Items;
import
hudson.model.JDK
;
import
hudson.model.Job
;
import
hudson.model.Label
;
import
hudson.model.ParameterDefinition
;
import
hudson.model.ParametersDefinitionProperty
;
import
hudson.model.Queue.FlyweightTask
;
import
hudson.model.Result
;
import
hudson.model.SCMedItem
;
...
...
@@ -89,6 +91,8 @@ import java.util.Map.Entry;
import
java.util.Set
;
import
java.util.logging.Level
;
import
java.util.logging.Logger
;
import
java.util.regex.Matcher
;
import
java.util.regex.Pattern
;
/**
* {@link Job} that allows you to run multiple different configurations
...
...
@@ -603,9 +607,11 @@ public class MatrixProject extends AbstractProject<MatrixProject,MatrixBuild> im
}
// find all active configurations
Set
<
MatrixConfiguration
>
active
=
new
LinkedHashSet
<
MatrixConfiguration
>();
final
Set
<
MatrixConfiguration
>
active
=
new
LinkedHashSet
<
MatrixConfiguration
>();
final
boolean
isDynamicFilter
=
isDynamicFilter
(
getCombinationFilter
());
for
(
Combination
c
:
activeCombinations
)
{
if
(
c
.
evalGroovyExpression
(
axes
,
combinationFilter
))
{
if
(
isDynamicFilter
||
c
.
evalGroovyExpression
(
axes
,
getCombinationFilter
()
))
{
LOGGER
.
fine
(
"Adding configuration: "
+
c
);
MatrixConfiguration
config
=
configurations
.
get
(
c
);
if
(
config
==
null
)
{
...
...
@@ -622,6 +628,27 @@ public class MatrixProject extends AbstractProject<MatrixProject,MatrixBuild> im
return
active
;
}
private
boolean
isDynamicFilter
(
final
String
filter
)
{
if
(!
isParameterized
()
||
filter
==
null
)
return
false
;
final
ParametersDefinitionProperty
paramDefProp
=
getProperty
(
ParametersDefinitionProperty
.
class
);
for
(
final
ParameterDefinition
definition
:
paramDefProp
.
getParameterDefinitions
())
{
final
String
name
=
definition
.
getName
();
final
Matcher
matcher
=
Pattern
.
compile
(
"\\b"
+
name
+
"\\b"
)
.
matcher
(
filter
)
;
if
(
matcher
.
find
())
return
true
;
}
return
false
;
}
private
File
getConfigurationsDir
()
{
return
new
File
(
getRootDir
(),
"configurations"
);
}
...
...
core/src/main/java/hudson/model/Cause.java
浏览文件 @
69de461c
...
...
@@ -148,6 +148,39 @@ public abstract class Cause {
this
.
upstreamCauses
=
upstreamCauses
;
}
/**
* @since 1.515
*/
@Override
public
boolean
equals
(
Object
rhs
)
{
if
(
this
==
rhs
)
return
true
;
if
(!(
rhs
instanceof
UpstreamCause
))
return
false
;
final
UpstreamCause
o
=
(
UpstreamCause
)
rhs
;
if
(
upstreamBuild
!=
o
.
upstreamBuild
)
return
false
;
if
(!
upstreamCauses
.
equals
(
o
.
upstreamCauses
))
return
false
;
if
(
upstreamUrl
==
null
?
o
.
upstreamUrl
!=
null
:
!
upstreamUrl
.
equals
(
o
.
upstreamUrl
))
return
false
;
if
(
upstreamProject
==
null
?
o
.
upstreamProject
!=
null
:
!
upstreamProject
.
equals
(
o
.
upstreamProject
))
return
false
;
return
true
;
}
/**
* @since 1.515
*/
@Override
public
int
hashCode
()
{
int
hashCode
=
17
;
hashCode
=
hashCode
*
31
+
upstreamCauses
.
hashCode
();
hashCode
=
hashCode
*
31
+
upstreamBuild
;
hashCode
=
hashCode
*
31
+
(
upstreamUrl
==
null
?
0
:
upstreamUrl
.
hashCode
());
return
hashCode
*
31
+
(
upstreamProject
==
null
?
0
:
upstreamProject
.
hashCode
());
}
private
@Nonnull
Cause
trim
(
@Nonnull
Cause
c
,
int
depth
,
Set
<
String
>
traversed
)
{
if
(!(
c
instanceof
UpstreamCause
))
{
return
c
;
...
...
core/src/test/java/hudson/matrix/CombinationFilterUsingBuildParamsTest.java
0 → 100644
浏览文件 @
69de461c
/*
* The MIT License
*
* Copyright (c) 2012, RedHat Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package
hudson.matrix
;
import
static
org
.
mockito
.
Matchers
.
anyInt
;
import
static
org
.
mockito
.
Mockito
.
mock
;
import
static
org
.
mockito
.
Mockito
.
never
;
import
static
org
.
mockito
.
Mockito
.
times
;
import
static
org
.
mockito
.
Mockito
.
verify
;
import
static
org
.
mockito
.
Mockito
.
when
;
import
hudson.ExtensionList
;
import
hudson.matrix.MatrixBuild.MatrixBuildExecution
;
import
hudson.matrix.listeners.MatrixBuildListener
;
import
hudson.model.AbstractItem
;
import
hudson.model.BuildListener
;
import
hudson.model.Cause
;
import
hudson.model.Node
;
import
hudson.model.ParametersAction
;
import
hudson.model.Result
;
import
hudson.model.Run
;
import
hudson.model.StringParameterValue
;
import
java.io.IOException
;
import
java.io.PrintStream
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.HashMap
;
import
java.util.HashSet
;
import
java.util.List
;
import
java.util.Map
;
import
jenkins.model.Jenkins
;
import
org.apache.commons.io.output.ByteArrayOutputStream
;
import
org.junit.Before
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
import
org.mockito.Mock
;
import
org.mockito.MockitoAnnotations
;
import
org.mockito.verification.VerificationMode
;
import
org.powermock.api.mockito.PowerMockito
;
import
org.powermock.core.classloader.annotations.PrepareForTest
;
import
org.powermock.modules.junit4.PowerMockRunner
;
/**
* Make sure that the combination filter schedules correct builds in correct order
*
* @author ogondza
*/
@RunWith
(
PowerMockRunner
.
class
)
@PrepareForTest
(
{
Jenkins
.
class
,
MatrixBuildListener
.
class
,
MatrixProject
.
class
,
AbstractItem
.
class
})
public
class
CombinationFilterUsingBuildParamsTest
{
/**
* Execute releases: experimental, stable, beta, devel
*
* x s b d
* 0.1
* 0.9 * * * *
* 1 * * *
* 2 * *
* 3 *
*/
private
static
final
String
filter
=
String
.
format
(
"(%s) || (%s) || (%s)"
,
"RELEASE == 'stable' && VERSION == '1'"
,
"RELEASE == 'beta' && VERSION >= '1' && VERSION <= '2'"
,
"RELEASE == 'devel' && VERSION >= '1' && VERSION <= '3'"
);
private
static
final
String
touchstoneFilter
=
"VERSION == '0.9'"
;
private
static
final
List
<
String
>
releases
=
Arrays
.
asList
(
"stable"
,
"beta"
,
"devel"
,
"experimental"
);
private
final
Map
<
String
,
MatrixConfiguration
>
confs
=
new
HashMap
<
String
,
MatrixConfiguration
>();
private
final
MatrixExecutionStrategy
strategy
=
new
DefaultMatrixExecutionStrategyImpl
(
true
,
touchstoneFilter
,
Result
.
SUCCESS
,
new
NoopMatrixConfigurationSorter
()
);
private
MatrixProject
project
;
@Mock
private
MatrixBuildExecution
execution
;
@Mock
private
MatrixBuild
build
;
@Mock
private
MatrixRun
run
;
@Mock
private
BuildListener
listener
;
@Mock
private
Jenkins
jenkins
;
@Mock
private
ExtensionList
<
MatrixBuildListener
>
extensions
;
@Before
public
void
setUp
()
throws
Exception
{
MockitoAnnotations
.
initMocks
(
this
);
usingDummyJenkins
();
usingNoListeners
();
usingDummyProject
();
usingDummyExecution
();
withReleaseAxis
(
releases
);
}
@Test
public
void
testCombinationFilterV01
()
throws
InterruptedException
,
IOException
{
givenTheVersionIs
(
"0.1"
);
strategy
.
run
(
execution
);
wasNotBuilt
(
confs
.
get
(
"devel"
));
wasNotBuilt
(
confs
.
get
(
"beta"
));
wasNotBuilt
(
confs
.
get
(
"stable"
));
wasNotBuilt
(
confs
.
get
(
"experimental"
));
}
@Test
public
void
testCombinationFilterV09
()
throws
InterruptedException
,
IOException
{
givenTheVersionIs
(
"0.9"
);
strategy
.
run
(
execution
);
wasBuilt
(
confs
.
get
(
"devel"
));
wasBuilt
(
confs
.
get
(
"beta"
));
wasBuilt
(
confs
.
get
(
"stable"
));
wasBuilt
(
confs
.
get
(
"experimental"
));
}
@Test
public
void
testCombinationFilterV1
()
throws
InterruptedException
,
IOException
{
givenTheVersionIs
(
"1"
);
strategy
.
run
(
execution
);
wasBuilt
(
confs
.
get
(
"devel"
));
wasBuilt
(
confs
.
get
(
"beta"
));
wasBuilt
(
confs
.
get
(
"stable"
));
wasNotBuilt
(
confs
.
get
(
"experimental"
));
}
@Test
public
void
testCombinationFilterV2
()
throws
InterruptedException
,
IOException
{
givenTheVersionIs
(
"2"
);
strategy
.
run
(
execution
);
wasBuilt
(
confs
.
get
(
"devel"
));
wasBuilt
(
confs
.
get
(
"beta"
));
wasNotBuilt
(
confs
.
get
(
"stable"
));
wasNotBuilt
(
confs
.
get
(
"experimental"
));
}
@Test
public
void
testCombinationFilterV3
()
throws
InterruptedException
,
IOException
{
givenTheVersionIs
(
"3"
);
strategy
.
run
(
execution
);
wasBuilt
(
confs
.
get
(
"devel"
));
wasNotBuilt
(
confs
.
get
(
"beta"
));
wasNotBuilt
(
confs
.
get
(
"stable"
));
wasNotBuilt
(
confs
.
get
(
"experimental"
));
}
private
void
usingDummyProject
()
{
project
=
PowerMockito
.
mock
(
MatrixProject
.
class
);
PowerMockito
.
when
(
build
.
getParent
()).
thenReturn
(
project
);
PowerMockito
.
when
(
project
.
getUrl
()).
thenReturn
(
"/my/project/"
);
when
(
project
.
getAxes
()).
thenReturn
(
new
AxisList
(
new
Axis
(
"RELEASE"
,
releases
)));
when
(
project
.
getCombinationFilter
()).
thenReturn
(
filter
);
}
private
void
usingDummyExecution
()
{
when
(
execution
.
getProject
()).
thenReturn
(
project
);
when
(
execution
.
getBuild
()).
thenReturn
(
build
);
when
(
execution
.
getListener
()).
thenReturn
(
listener
);
// throw away logs
when
(
listener
.
getLogger
()).
thenReturn
(
new
PrintStream
(
new
ByteArrayOutputStream
()
));
// Succeed immediately
when
(
run
.
isBuilding
()).
thenReturn
(
false
);
when
(
run
.
getResult
()).
thenReturn
(
Result
.
SUCCESS
);
}
private
void
usingDummyJenkins
()
{
PowerMockito
.
mockStatic
(
Jenkins
.
class
);
when
(
Jenkins
.
getInstance
()).
thenReturn
(
jenkins
);
when
(
jenkins
.
getNodes
()).
thenReturn
(
new
ArrayList
<
Node
>());
}
private
void
usingNoListeners
()
{
when
(
extensions
.
iterator
()).
thenReturn
(
new
ArrayList
<
MatrixBuildListener
>().
iterator
());
when
(
MatrixBuildListener
.
all
()).
thenReturn
(
extensions
);
}
private
void
withReleaseAxis
(
final
List
<
String
>
releases
)
{
for
(
final
String
release:
releases
)
{
confs
.
put
(
release
,
getConfiguration
(
"RELEASE="
+
release
));
}
when
(
execution
.
getActiveConfigurations
()).
thenReturn
(
new
HashSet
<
MatrixConfiguration
>(
confs
.
values
())
);
}
private
MatrixConfiguration
getConfiguration
(
final
String
axis
)
{
final
MatrixConfiguration
conf
=
mock
(
MatrixConfiguration
.
class
);
when
(
conf
.
getParent
()).
thenReturn
(
project
);
when
(
conf
.
getCombination
()).
thenReturn
(
Combination
.
fromString
(
axis
));
when
(
conf
.
getDisplayName
()).
thenReturn
(
axis
);
when
(
conf
.
getUrl
()).
thenReturn
(
axis
);
when
(
conf
.
getBuildByNumber
(
anyInt
())).
thenReturn
(
run
);
return
conf
;
}
private
void
givenTheVersionIs
(
final
String
version
)
{
final
ParametersAction
parametersAction
=
new
ParametersAction
(
new
StringParameterValue
(
"VERSION"
,
version
)
);
when
(
build
.
getAction
(
ParametersAction
.
class
))
.
thenReturn
(
parametersAction
)
;
}
private
void
wasBuilt
(
final
MatrixConfiguration
conf
)
{
wasBuildTimes
(
conf
,
times
(
1
));
}
private
void
wasNotBuilt
(
final
MatrixConfiguration
conf
)
{
wasBuildTimes
(
conf
,
never
());
}
private
void
wasBuildTimes
(
final
MatrixConfiguration
conf
,
final
VerificationMode
mode
)
{
verify
(
conf
,
mode
).
scheduleBuild
(
new
ArrayList
<
MatrixChildAction
>(),
new
Cause
.
UpstreamCause
((
Run
<?,
?>)
build
)
);
}
}
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录