bug 说明:对于NumberUtils中的createNumber(String)函数,当输入的字符串的长度刚好是long或integer的极限长度时,由于缺乏考虑最高位为1(符号位,即负数)的情况导致出错。
修复过程描述:(代码,用时30mins,涉及单个函数)
(1)错误定位:注释掉不相关的错误测试,跑测试,定位到crash的位置。
(2)观察出错的测试输入数据是否具有特殊性,比如此处出错时的输入数据是字符串形式表示的数值量,根据函数的功能(根据函数名称以及测试获得)知道其将输入的字符串转换为数值对象,其目标是尽可能用小的数值类型进行表示。比如能用integer进行表示的,不用long类型。根据经验我们知道当将字符串转换为数值时需要考虑其表示的最大位数和正负性。而根据eclipse输出的出错信息得知其刚好是涉及到正负相关,而程序中对临界值没有特殊考虑,修复方案添加临界边值的判断条件:(hexDigits == 16 && str.charAt(pfxLen) > '7')和(hexDigits == 8 && str.charAt(pfxLen) > '7')。但是测试发现还是存在错误,原因在于测试输入的数据是0x00f000000时,生成了long类型数据,期望的是integer数据,观察知道数据,对于数据的最高位需要进行0过滤,因此添加了下面代码的最上面的while循环,最终测试通过。
while(pfxLen < str.length() && str.charAt(pfxLen) == '0'){ pfxLen ++;}final int hexDigits = str.length() - pfxLen;if (hexDigits > 16 || (hexDigits == 16 && str.charAt(pfxLen) > '7')) { return createBigInteger(str);}if (hexDigits > 8 || (hexDigits == 8 && str.charAt(pfxLen) > '7')) { return createLong(str);}return createInteger(str);标准patch:
if (pfxLen > 0) { // we have a hex number- char firstSigDigit = 0; // strip leading zeroes- for(int i = pfxLen; i < str.length(); i++) {- firstSigDigit = str.charAt(i);- if (firstSigDigit == '0') { // count leading zeroes- pfxLe1 n++;- } else {- break;- }- }final int hexDigits = str.length() - pfxLen;- if (hexDigits > 16 || (hexDigits == 16 && firstSigDigit > '7')) { // too many for Long+ if (hexDigits > 16) { // too many for Long return createBigInteger(str); }- if (hexDigits > 8 || (hexDigits == 8 && firstSigDigit > '7')) { // too many for an int+ if (hexDigits > 8) { // too many for an int return createLong(str); }return createInteger(str);
IllegalArgumentException。bug说明:针对java 7,对Locale的variant没有验证处理,针对ja_JP_JP_#u-ca-japanese和th_TH_TH_#u-nu-thai,根据测试要求应该输出异常
修复说明:(代码+注释(辅助理解程序),用时1.5h,涉及单个函数)
首先跑测试找到失败的测试用例ja_JP_JP_#u-ca-japanese和th_TH_TH_#u-nu-thai,然后单步跟踪执行发现能够正常执行,没有发生异常(目标是产生异常)。因此在测试用例中寻找相似的测试用例进行测试对比,发现相似的测试输入有sr_ME_#Latn、sr_BA_#Latn、sr__#Latn以及sr_RS_#Latn,并且都能够正常通过测试。同样使用单步调试,发现当输入的Locale为上面4种时,测试函数并没有调用toLocale(String)函数,而未通过的两个测试输入都调用了该函数,且产生了不是期望的结果。因为没有执行出错的函数,因此没有产生修复帮助,无可比性。然后对比其他的测试输入,发现通过的测试用例调用该函数时传入的参数中都没有"#"字符以及之后的字符,因此生成了如下的修复方案:
if(str.contains("#")){ throw new IllegalArgumentException("Invalid locale format: " + str);}运行测试,结果正确。但是感觉修复得很trivial,并没有搞懂真正的原因是什么,后来查看修复的patch,结果就是这样修复的。再上网查,原因是java 1.7对Locale的variant做了严格的验证导致的,具体怎么做的不是很清楚,但是通过上面的修复可以解决这个问题,但是应该还是算是一个取巧的方法。
标准patch:
- if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions- throw new IllegalArgumentException("Invalid locale format: " + str);- } final int len = str.length();
bug说明:将字符串转换为为数字,Float和Double类型由于精度不够导致转换之后的数字截断
修复过程描述:(代码,用时20min,涉及单个函数)
根据测试(assert信息)很容易发现字符串转换为数字导致精度不够,定位到createNumber(String)函数的最后几行,由代码知道,其策略是从精度由低到高尝试创建数据类型(Float->Double->BigDecimal),然后判断当前创建的数据是否满足精度要求决定是否提高精度,通过打印输入的字符串和生成的数据对比发现,对于创建的数据的精度的判断不够严格,导致创建的数据已经产生截断而没有被发现。通过通读该函数代码发现,之前已经解析出字符串所代表数字的整数部分,小数部分和指数部分,以及对应的长度。因此,直接通过对应数据类型的精度位数和小数部分的长度进行对比即可,为了修改最小,直接在原始条件的基础之上添加小数位数的判断,修复代码如下:
try { final Float f = createFloat(str); if (!(f.isInfinite() || (f.floatValue() == 0.0F && !allZeros) || numDecimals > 7)) { return f; }} catch (final NumberFormatException nfe) { // NOPMD // ignore the bad number}try { final Double d = createDouble(str); if (!(d.isInfinite() || (d.doubleValue() == 0.0D && !allZeros) || numDecimals > 16)) { return d; }} catch (final NumberFormatException nfe) { // NOPMD// ignore the bad number}return createBigDecimal(str);标准patch:
try {- if(numDecimals <= 7){// If number has 7 or fewer digits past the decimal point then make it a float final Float f = createFloat(str); if (!(f.isInfinite() || (f.floatValue() == 0.0F && !allZeros))) { return f; }- }} catch (final NumberFormatException nfe) { // NOPMD // ignore the bad number}try {- if(numDecimals <= 16){// If number has between 8 and 16 digits past the decimal point then make it a double final Double d = createDouble(str); if (!(d.isInfinite() || (d.doubleValue() == 0.0D && !allZeros))) { return d; }- }
bug说明:在Map表中查询元素CharSequence类型数据返回一直为null,原因很明显是查询操作get(key)在比较key值的时候依据hash值进行比较的
修复过程描述:(代码,用时10mins,涉及单个函数)
阅读源代码我们知道函数的功能是从HashMap中查询数据,出错的原因是内容相同的两个CharSequence实例在应用到HashMap的查询上时,由于其查询依据的是key的HashCode信息,导致查询结果为空,因此很容易想到的修复方案是将比较的两个实例转换为可以进行内容对比的同等实例。最容易想到的就是String类,故修复方案如下:(改动最小方案,只涉及到一个函数。其他修改方法涉及其他函数,比如存入HashMap中的类型改成String)
// 修复之前代码// descend so as to get a greedy algorithmfor (int i = max; i >= shortest; i--) { final CharSequence subSeq = input.subSequence(index, index + i); final CharSequence result = lookupMap.get(subSeq); if (result != null) { out.write(result.toString()); return i; }}//修复之后代码// descend so as to get a greedy algorithmfor (int i = max; i >= shortest; i--) { final CharSequence subSeq = input.subSequence(index, index + i); final String query = new String(new StringBuilder(subSeq)); for(CharSequence charSequence : lookupMap.keySet()){ final String string = new String(new StringBuffer(charSequence)); if(query.equals(string)){ final CharSequence result = lookupMap.get(charSequence); out.write(result.toString()); return i; } }}标准patch:
public class LookupTranslator extends CharSequenceTranslator { - private final HashMap<String, CharSequence> lookupMap;+ private final HashMap<CharSequence, CharSequence> lookupMap; private final int shortest; private final int longest; -43,12 +43,12 public class LookupTranslator extends CharSequenceTranslator { * lookup CharSequence[][] table of size [*][2] */ public LookupTranslator(final CharSequence[]... lookup) {- lookupMap = new HashMap<String, CharSequence>();+ lookupMap = new HashMap<CharSequence, CharSequence>(); int _shortest = Integer.MAX_VALUE; int _longest = 0; if (lookup != null) { for (final CharSequence[] seq : lookup) {- this.lookupMap.put(seq[0].toString(), seq[1]);+ this.lookupMap.put(seq[0], seq[1]); final int sz = seq[0].length(); if (sz < _shortest) { _shortest = sz; -74,7 +74,7 public class LookupTranslator extends CharSequenceTranslator { // descend so as to get a greedy algorithm for (int i = max; i >= shortest; i--) { final CharSequence subSeq = input.subSequence(index, index + i);- final CharSequence result = lookupMap.get(subSeq.toString());+ final CharSequence result = lookupMap.get(subSeq); if (result != null) { out.write(result.toString()); return i;
Locale的解析过程通常情况下输入的是xx_XX_yyy格式(2)根据测试输入的是_XX_yyy格式时应该正确解析(3)观察发现xx和XX是相同的内容,只是区分了大小写。bug说明:对于Locale的解析,当语言为空只有国家时,如_GB,要求能正常解析
修复过程描述:(源代码,用时20mins,涉及单个函数)
运行测试程序发现,当测试的输入是以_开头时,原来的代码不能进行解析,会抛出异常,因为原始的代码根据要求前两个字母是语言标志,一定是小写字母,但是没有考虑不包含语言标志的情况,即直接以_开头。所以需要对输入的字符串的首字母进行判断,再结合测试中所有以_开头的测试输入的期待输出结果,可以得出结论:对于以_开头的字符串,可以跳过前两个字符的比较,后面的字符的判断和可以套用输入存在语言标志的情况。因此分类处理如下:情况一,非_开头的要求一定是以小写字母开头,即保持原来的不变;情况二,当以_开头时,类似于在原来代码从第三个字符之后的判断。因为代码中用的是常数数字对每一位进行判断,而且输入的字符串是final类型,因此需要添加一个完整的分支,将所有对应的每一位的判断填充上,因此修复方案如下:
//修复之前代码final char ch0 = str.charAt(0); final char ch1 = str.charAt(1);...return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));//修复之后代码final char ch0 = str.charAt(0); if(ch0 != '_'){ final char ch1 = str.charAt(1); ... return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));}else{ if (len < 3) { throw new IllegalArgumentException("Invalid locale format: " + str); } final char ch1 = str.charAt(1); if (ch1 == '_') { throw new IllegalArgumentException("Invalid locale format: " + str); } final char ch2 = str.charAt(2); if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) { throw new IllegalArgumentException("Invalid locale format: " + str); } if (len == 3) { return new Locale("", str.substring(1, 3)); } if (len < 5) { throw new IllegalArgumentException("Invalid locale format: " + str); } if (str.charAt(3) != '_') { throw new IllegalArgumentException("Invalid locale format: " + str); } return new Locale("", str.substring(1, 3), str.substring(4));}标准patch:
final char ch0 = str.charAt(0);- if (ch0 == '_') {- if (len < 3) {- throw new IllegalArgumentException("Invalid locale format: " + str);- }- final char ch1 = str.charAt(1);- final char ch2 = str.charAt(2);- if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {- throw new IllegalArgumentException("Invalid locale format: " + str);- }- if (len == 3) {- return new Locale("", str.substring(1, 3));- }- if (len < 5) {- throw new IllegalArgumentException("Invalid locale format: " + str);- }- if (str.charAt(3) != '_') {- throw new IllegalArgumentException("Invalid locale format: " + str);- }- return new Locale("", str.substring(1, 3), str.substring(4));- } else { final char ch1 = str.charAt(1); if (!Character.isLowerCase(ch0) || !Character.isLowerCase(ch1)) { throw new IllegalArgumentException("Invalid locale format: " + str); -145,7 +125,6 public class LocaleUtils { throw new IllegalArgumentException("Invalid locale format: " + str); } return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));- }
Exception(5)循环变量受字符串的长度约束bug说明:访问字符串中的字符时越界
修复过程描述:(源代码,用时10mins,涉及单个函数)
通过测试定位到出错的代码行,然后根据错误信息发现是字符串的越界访问错误,定位到了一处字符计数代码。只是根据代码的命名大体猜一下代码的功能,并没有完全理解。但是在观察代码的时候发现:在循环的语句中并没有用到循环计数器,而这个循环计数器根据之前的代码应该是和字符串的长度相关,同时与循环体里用到的变量名称非常相似,极易混淆,所以尝试修改函数体里的字符串的下标变量:将pos修改为pt,重新跑测试,成功通过。
//修改之前的代码for (int pt = 0; pt < consumed; pt++) { pos += Character.charCount(Character.codePointAt(input, pos));}//修改之后的代码 将'pos'改成了'pt'for (int pt = 0; pt < consumed; pt++) { pos += Character.charCount(Character.codePointAt(input, pt));}标准patch:
for (int pt = 0; pt < consumed; pt++) {- pos += stringCharacter.charCount(Character.codePointAt(input, pt));+ pos += Character.charCount(Character.codePointAt(input, pos)); }
- -开头时期望抛出NumberFormatException(2)程序实际返回了null。bug说明:在将字符串转换为数值时,以- -开头的字符串应该抛出异常,但是程序返回了null。
修复过程描述:(源代码,用时5分钟,涉及单个函数)
运行测试直接定位到错误代码行,根据错误信息:当测试代码的输入是- - 1.1E-700F时应该抛出异常,但是查看代码发现,对于以- -开头的字符串直接返回了null,不符合测试的要求。因此直接改成期待的输出即可。修改如下:
if (str.startsWith("--")) {// return null; //原始代码,下面一行为替换代码 throw new NumberFormatException("Starting with '--' is not a valid number");}标准patch:
if (StringUtils.isBlank(str)) { throw new NumberFormatException("A blank string is not a valid number"); } + if (str.startsWith("--")) {+ return null;+ } if (str.startsWith("0x") || str.startsWith("-0x") || str.startsWith("0X") || str.startsWith("-0X")) { int hexDigits = str.length() - 2; // drop 0x if (str.startsWith("-")) { // drop - -715,13 +718,10 public class NumberUtils { if (StringUtils.isBlank(str)) { throw new NumberFormatException("A blank string is not a valid number"); }- if (str.trim().startsWith("--")) { // this is protection for poorness in java.lang.BigDecimal. // it accepts this as a legal value, but it does not appear // to be in specification of class. OS X Java parses it to // a wrong value.- throw new NumberFormatException(str + " is not a valid number.");- } return new BigDecimal(str);
bug说明:在生成时间时所用的时区不是传入参数指定的时区
修复过程描述:(源代码,用时10分钟,涉及一个函数)
单步调试测试用例,定位到解析时间函数中。在调试的过程中发现,期待的时区和传入的calendar变量的时区是一致的,而最后生成的时间所用的时区是不一致的,因此确定是时区的解析有问题。在定位到时区解析函数后,发现传入的calendar变量没有使用,因此确定错误是应该将里边的默认TimeZone修改成传入的参数。修复方案如下,成功通过所有测试。
//原始代码if (zone.useDaylightTime() && calendar.get(Calendar.DST_OFFSET) != 0) { buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));} else { buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));}//修复代码, 最开始的考虑是将添加’zone=calendar.getTimeZone()',但是zone是final类型,不能修改,只能改成如下样子,然后为了确认逻辑正确,寻找zone的赋值位置,发现只有在实例化的时候才会被赋值,而该测试在实例化的时候调用的是不传入zone的构造函数,因此只能修改如下if (calendar.getTimeZone().useDaylightTime() && calendar.get(Calendar.DST_OFFSET) != 0) { buffer.append(getTimeZoneDisplay(calendar.getTimeZone(), true, mStyle, mLocale));} else { buffer.append(getTimeZoneDisplay(calendar.getTimeZone(), false, mStyle, mLocale));}标准patch:
private static class TimeZoneNameRule implements Rule { private final Locale mLocale; private final int mStyle;+ private final TimeZone zone; private final String mStandard; private final String mDaylight; -1108,6 +1109,7 public class FastDatePrinter implements DatePrinter, Serializable { TimeZoneNameRule(TimeZone timeZone, Locale locale, int style) { mLocale = locale; mStyle = style;+ zone = timeZone; mStandard = getTimeZoneDisplay(timeZone, false, style, locale); mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); -1129,7 +1131,6 public class FastDatePrinter implements DatePrinter, Serializable { */ public void appendTo(StringBuffer buffer, Calendar calendar) {- TimeZone zone = calendar.getTimeZone(); if (zone.useDaylightTime() && calendar.get(Calendar.DST_OFFSET) != 0) { buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));
'd'd时正常,当输入参数是'd'd'时应该抛出异常(2)上网查找发现对字符串的正则解析过程中,字符串中的引号应该是偶数(3)在单步调试过程中发现对'd'd'的解析过程没有解析完全,当解析到最后一个d的时候就返回了bug说明:不能识别出含有未配对引号的字符串
修复过程描述:(源代码+注释+上网搜索,用时1h,涉及单个函数)
首先是跑测试发现在应该出现异常时,程序正常运行。对比两组测试用例,发现唯一的不同是正则表达式的字符串不同。根据单步调试,找出了在直接调用系统的函数时,抛出异常的原因是最后的单个引号不能被解析('d'd')。而代码中解析的结果是没有出现异常。再根据注释的信息发现期待有unterminated quote异常,因此上网搜索与regex相关的该异常,在StackOverflow上发现有人说传入的字符串中的'或"应该是偶数出现。然后比较正确的测试输入'd'd确定错误是被解析的字符串中引号的数量是奇数。因此寻找对字符串进行正则表达式解析的代码块,然后单步调试比较成功测试和失败测试在该解析过程中的变量的变化情况。发现成功测试应该是把完整的字符串解析,而在解析'd'd'字符串时,解析到最后一个d就结束了,直接退出,因此通过对字符串的解析完整度的判断是一个可行的补丁。修复方法如下,正确通过所有测试。
if(patternMatcher.regionStart() < patternMatcher.regionEnd()){ throw new IllegalArgumentException("unterminated quote");}标准patch:
- if (patternMatcher.regionStart() != patternMatcher.regionEnd()) {- throw new IllegalArgumentException("Failed to parse \""+pattern+"\" ; gave up at index "+patternMatcher.regionStart());- }
bug说明:空格的匹配需要严格数量一致
修复过程说明:(源代码,用时1.5h,涉及单个函数)
运行测试程序,发现使用系统的函数对M E进行正则表达式解析之后,期望的结果是不能匹配3 Tue,而实际的输出是可以正常匹配的。因此判断出错原因是生成的正则表达式错误,但是尝试使用单步调试时发现系统生成的正则表达式的pattern是看不到的,因此不能直接比较其生成的pattern的区别。所以,使用单步调试观察系统函数的匹配过程,发现其匹配的时候是按位进行匹配的。根据上面的数据知道M E中间只有一个空格,而3 Tue中间有两个空格字符导致其不能匹配成功。反过来再观察出错程序的解析过程发现生成的pattern中对空格的匹配是\s*+,因此导致多个空格也可以正常匹配。找到了真正的原因,开始找M E转pattern的功能代码,发现代码内将空格直接解析成了\s*+,修改方法是改成单个空格。修复方法如下,正确通过所有测试。
boolean wasWhite= false;for(int i= 0; i<value.length(); ++i) { char c= value.charAt(i);// 修复方法就是将下面的几行代码注释掉,当字符串中是空格时,在pattern中就用空格代替,使匹配的空格的数量一致// if(Character.isWhitespace(c)) {// if(!wasWhite) {// wasWhite= true;// regex.append("\\s*+");// }// continue;// } wasWhite= false; switch(c) { case '\'': if(unquote) { if(++i==value.length()) { return regex; } c= value.charAt(i); } break; case '?': ... case '.': regex.append('\\'); } regex.append(c);标准patch:
+ boolean wasWhite= false; for(int i= 0; i<value.length(); ++i) { char c= value.charAt(i);+ if(Character.isWhitespace(c)) {+ if(!wasWhite) {+ wasWhite= true;+ regex.append("\\s*+");+ }+ continue;+ }+ wasWhite= false;
bug说明:将double数转换为分数表示,在double类型数比较小的时候可能会出现整数超出范围
修复过程描述:(源代码+注释,用时1h,涉及单个函数)
通过跑测试发现,当输入的测试数据是0.5000000001时,期待的数据是1/2,在实际运行过程中却抛出了异常,根据源代码,发现通过迭代方式存在两组变量相互迭代更新,p1,q1保存上一次迭代的结果,p2,q2保存用于下一次更新的结果,当抛出异常时,输出对应的数据发现p1,q1刚好满足要求,而p2,q2溢出,结合注释说明:“当生成的分数与实数已经很接近的时候返回上一次(p1,q1)迭代的结果”。因此修复方案如下:
// BigFraction.java//原始代码if ((p2 > overflow) || (q2 > overflow)) { // in maxDenominator mode, if the last fraction was very close to the actual value // q2 may overflow in the next iteration; in this case return the last one. throw new FractionConversionException(value, p2, q2);}//修复代码if ((p2 > overflow) || (q2 > overflow)) { // in maxDenominator mode, if the last fraction was very close to the actual value // q2 may overflow in the next iteration; in this case return the last one. //throw new FractionConversionException(value, p2, q2); break;}然而,当修复之后,发现Fraction.java中的错误信息和BigFraction.java的类似,因此生成同样的修复。再次跑测试,但是当测试的输入为0.75000000001455192时又期望产生异常,按照上面的修复方案是不能生成异常的。由于两处bug相似,开始思考针对两种不同的输入但是都同样产生越界为什么期望的结果不一样?我开始观察两组测试的不同点,发现在没有生成异常的测试中没有指明epsilon,而在期待生成异常的测试中指明了该精度,因此我想到的是如果没有人为的指定精度就返回一个精度最高的结果,如果已经指明精度就一定要达到精度,或者达不到精度要求而溢出,结合上面的分析,重新修改代码,修复方案如下:
if ((FastMath.abs(p2) > overflow) || (FastMath.abs(q2) > overflow)) { if(epsilon == 0){ //当用户没有指定精度时,系统默认为0 break; } // in maxDenominator mode, if the last fraction was very close to the actual value // q2 may overflow in the next iteration; in this case return the last one. throw new FractionConversionException(value, p2, q2);}应用上面的修复,可以正常跑过所有的测试。
标准patch:
-303,9 +303,6 public class BigFraction if ((p2 > overflow) || (q2 > overflow)) { // in maxDenominator mode, if the last fraction was very close to the actual value // q2 may overflow in the next iteration; in this case return the last one.- if (epsilon == 0.0 && FastMath.abs(q1) < maxDenominator) {- break;- } throw new FractionConversionException(value, p2, q2); } diff --git a/src/main/java/org/apache/commons/math3/fraction/Fraction.java b/src/main/java/org/apache/commons/math3/fraction/Fraction.javaindex 002dae9..8065885 100644--- a/src/main/java/org/apache/commons/math3/fraction/Fraction.java+++ b/src/main/java/org/apache/commons/math3/fraction/Fraction.java -212,9 +212,6 public class Fraction if ((FastMath.abs(p2) > overflow) || (FastMath.abs(q2) > overflow)) { // in maxDenominator mode, if the last fraction was very close to the actual value // q2 may overflow in the next iteration; in this case return the last one.- if (epsilon == 0.0 && FastMath.abs(q1) < maxDenominator) {- break;- } throw new FractionConversionException(value, p2, q2); }
bug说明:分布的一个采样,采样的结果不在期待的范围之内
修复过程描述:(代码+注释+上网,用时2h,涉及多个函数【因为不知道是哪个函数错误】)失败
根据测试首先定位错误的位置,但是由于测试时随机采样,是一个算法问题,因此首先是读懂算法实现,根据注释信息(一个wiki网页链接),关于一个取样的算法,但是没有找到具体的公式,需要依据函数公式进行逆推,而且最后测试的判断条件是一个范围,难以确定错误所在。估计需要懂该算法或者作者自己修复。(或者更长的修复时间)
标准patch:
public double getNumericalMean() {- return getSampleSize() * (getNumberOfSuccesses() / (double) getPopulationSize());+ return (double) (getSampleSize() * getNumberOfSuccesses()) / (double) getPopulationSize(); }
bug说明:计算线性组合时,当输入的数组长度为1时出现数组越界
修复过程描述:(代码,用时10mins,涉及单个函数)
根据出错的测试函数,以及exception信息定位到求解线性组合的函数中,通过读代码发现对输入的参数的长度没有进行判断就进行下标访问导致出错,采用最简单的修复方法:根据测试函数得知,当数组长度为1时,直接将两个数进行相乘即可。修复代码如下:
if(len == 1){ return a[0] * b[0];}标准patch:
- if (len == 1) { // Revert to scalar multiplication.- return a[0] * b[0];- }
null,(2)接下来对该变量进行解引用操作导致异常bug说明:在求两个向量的交点时由于对求交的返回是否为null缺少判断进行直接引用导致空指针错误
修复过程描述:(源代码,用时20mins,涉及单个函数)
根据测试以及错误输出定位信息,找到exception的代码行。根据代码的命名和阅读代码理解代码的作用是求两条直线的交点,而测试的输入是两条没有相交点的直线(平行线),因此应该返回为null,单步调试发现在调用intersection()函数之后返回两条直线的交点为null时,并没有进行特殊处理,而是直接作为参数传递给了另一个函数导致空指针错误。修复很容易,添加一个返回值判断,修复如下:
// 两处错误相同if(v1D == null){ return null;}标准patch:
-111,9 +111,6 public class SubLine { // compute the intersection on infinite line Vector3D v1D = line.intersection(subLine.line);- if (v1D == null) {- return null;- } // check location of point with respect to first sub-line Location loc1 = remainingRegion.checkPoint(line.toSubSpace(v1D));diff --git a/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/SubLine.java b/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/SubLine.javaindex ea9e96a..a9d621a 1006441--- a/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/SubLine.java+++ b/src/main/java/org/apache/commons/math3/geometry/euclidean/twod/SubLine.java -115,9 +115,6 public class SubLine extends AbstractSubHyperplane<Euclidean2D, Euclidean1D> { // compute the intersection on infinite line Vector2D v2D = line1.intersection(line2);- if (v2D == null) {- return null;- }
0时期望的结果是INF(2)程序在该情况下返回了NaNbug说明:求复数的倒数,当复数为0时,其倒数应该是INF,而程序中返回的是NaN
修复过程描述:(源代码,5mins,涉及单个函数)
根据测试,容易定位出错代码的位置。已有代码对这种特殊情况作了特殊处理,只是返回的值不是期望的值。修改成期望的值即可,修复如下。
if (real == 0.0 && imaginary == 0.0) {// return NaN; return INF;}标准patch:
if (real == 0.0 && imaginary == 0.0) {- return INF;+ return NaN; }
0,而期望的值应该是大于0的(2)根据该变量的名称判断是一个循环的标志量(3)调试定位发现错误应该出在doTest函数(4)函数的功能是迭代处理数据(5)在迭代的过程中有一个临时的循环变量计数,和未赋值的变量属于同类型,且名称接近(5)如果找到了该处并成功修改,则会发现依然会出现异常,调试发现原因是边界值maxIteration初始值是0,整个计算过程中没有被重新赋值bug说明:在测试优化时,当优化结束之后获取到的迭代次数始终为0,interations=0
修复过程描述:(代码+注释(帮助理解程序),用时3h,涉及多个函数)
根据测试获得很多未通过测试,发现未通过的测试主要集中在一个doTest函数,但是并不能确定出错具体位置,使用单步调试,跟踪运行结束之后还是一样出错,没有发现具体原因。根据测试函数,最后的变量iteration需要大于0,因此从逆向寻找问题的原因。首先查找iteration的更新函数,只有一个,从此处打印输出,再次运行测试,发现并没有调用该函数,因此确定错误原因是未调用函数,然后再次单步调试,尝试理解代码的大体流程,发现了一处进行迭代计算的代码,根据命名和执行的情况(iteration,循环),确定应该在该处进行iteration的更新,即调用更新函数。然后再次运行测试函数,结果出现了另一种错误,根据提示找到出错原因是程序中对maxIteration没有赋值(该值是iteration的上界,越界会触发异常),一直为0,导致iteration增长超过限定值而触发错误。然后开始逆向寻找maxIteration的更新位置,发现在初始化的时候对其进行赋值,还有一处是通过测试参数传入赋值。但是根据测试传入的参数情况maxIteration并没有被赋值,因此只能是在其创建的时候进行初始化赋值,找到对象的创建位置,发现创建时其值是默认0,将其改成最大的整数值(根据常识,最大的整数是边界值)。然后再运行测试,结果刚刚运行的一个测试函数正确通过。然后运行所有测试,发现还有很多错误,查看代码发现测试的运行流程基本一致,因此考虑存在不同的Optimizer类型,每个类型中都存在相同的问题。然后找到对应的重写函数发现果然都没有添加对应iteration的更新,反而是声明了一个局部变量iter。修改方法是将局部变量都修改成了全局变量iteration的更新,再次运行测试可以正常通过。一个示例修改如下:
PointValuePair current = null;int iter = 0;int maxEval = getMaxEvaluations();while (true) // ++iter; //此处修改为更新全局iteration,调用incrementIterationCount()函数 incrementIterationCount(); final double objective = computeObjectiveValue(point); PointValuePair previous = current; current = new PointValuePair(point, objective); if (previous != null) { if (checker.converged(getIterations(), previous, current)) {// if (checker.converged(iter, previous, current)) { //此处更改为获取全局iteration值// We have found an optimum. return current; } } ... // Compute conjugate search direction.// if (iter % n == 0 || //此处修改为获取全局iteration值 if (getIterations() % n == 0 || beta < 0) { // Break conjugation: reset search direction. searchDirection = steepestDescent.clone(); } else { // Compute new conjugate search direction. for (int i = 0; i < n; ++i) { searchDirection[i] = steepestDescent[i] + beta * searchDirection[i]; } }标准patch:
the same repair
bug说明:(1)对事件规划的函数,有一个时间标志量记录当前执行完事件之后的最新时间(2)每个事件都有一个保存当前事件上次执行结束之后的时间(3)当一部分事件执行,一部分事件没有执行的时候,没有执行的事件的时间标志会落后最新的时间导致出错
修复过程描述:(代码+注释,用时3.5h,涉及多个函数【需要确定多个函数中应该在哪里修复】)
运行未通过的测试函数,使用单元测试会打印出出错的stacktrace,根据提示可以找到出错的路径,但是因为是对求得的值进行判断,并不会crash,因此不能找到具体哪条语句出错,也就不能找到相关变量的情况。使用单步调试,发现事件规划时存在两个事件,当其中的一个事件被执行而另一个事件没有执行时,当前的事件规划器interpolator的时间会更新,同时被执行的事件的时间也会被更新,但是没有执行的事件的时间记录保持不变,因此时间会落后而导致出错。因此找与事件的时间更新相关的代码。找到了一个函数setStep(double)函数是对事件的时间进行更新,以及另一个函数reinitializeBegin()对时间也会更新。根据名称发现第二个函数似乎和初始化相关,因此首先考虑的是应该缺少setStep()函数的调用。再详细的阅读代码以及使用单步调试,发现在被执行的事件结束后退出之前有一处更新事件的时间语句,如下:
for (final EventState remaining : occuringEvents) { remaining.stepAccepted(eventT, eventY);}然而没有被更新时间的事件不被包含在occuringEvents集合中,因此考虑到是否是开发者错误的使用了变量,因此将里边的occuringEvents修改成了一个包含所有事件的集合eventsStates,然后运行测试,结果通过测试。然后运行其他的所有测试,结果又出现了新的测试错误。因此考虑此处的修改方法不正确。然后再观察注释信息,根据Accept a step, triggering events and step handlers注释内容,该函数是执行一步操作,并不一定涉及所有的事件的更新任务。因此往上一层调用该函数的位置查找原因。在调用的函数中发现,这样的语句:
if (!isLastStep) { // prepare next step interpolator.storeTime(stepStart); ...}根据变量的判断和注释信息,可以猜测,如果没有结束运行的话需要为下一轮的运行做准备(注释),同时语句interpolator.storeTime()是更新了当前的执行时间,因此猜测该处可能是更新信息的位置,而在该处并不能访问到上面的这些事件,只能通过其他间接的方法。注意到上面的更新时间信息还有一个函数,就是reinitializeBegain()。在执行事件之前,对是否需要初始化的一个标志变量进行了判断,如果需要初始化则调用该函数,因此只要标记一下该标志量就好了。找到了更新该标志量的接口函数,修改方案如下:
if (!isLastStep) { // prepare next step interpolator.storeTime(stepStart); setStateInitialized(false); //此处为添加内容,使得下一轮运行的时候能够更新所有事件的时间信息 ...}修改之后运行所有测试通过。
标准patch:
-343,10 +343,8 public abstract class AbstractIntegrator implements FirstOrderIntegrator { final double[] eventY = interpolator.getInterpolatedState().clone(); // advance all event states to current time- for (final EventState state : eventsStates) {- state.stepAccepted(eventT, eventY);- isLastStep = isLastStep || state.stop();- }+ currentEvent.stepAccepted(eventT, eventY);+ isLastStep = currentEvent.stop(); // handle the first part of the step, up to the event for (final StepHandler handler : stepHandlers) { -356,19 +354,22 public abstract class AbstractIntegrator implements FirstOrderIntegrator { if (isLastStep) { // the event asked to stop integration System.arraycopy(eventY, 0, y, 0, y.length);+ for (final EventState remaining : occuringEvents) {+ remaining.stepAccepted(eventT, eventY);+ } return eventT; } - boolean needReset = false;- for (final EventState state : eventsStates) {- needReset = needReset || state.reset(eventT, eventY);- }+ boolean needReset = currentEvent.reset(eventT, eventY); if (needReset) { // some event handler has triggered changes that // invalidate the derivatives, we need to recompute them System.arraycopy(eventY, 0, y, 0, y.length); computeDerivatives(eventT, y, yDot); resetOccurred = true;+ for (final EventState remaining : occuringEvents) {+ remaining.stepAccepted(eventT, eventY);+ } return eventT; }
List中的元素进行采样并保存到该数组中(3)数组的初始化类型是使用了List中的第一个元素的类型(4)放入List中元素的类型是不确定的,需要都兼容bug说明:由于给数组赋值了不兼容的类型导致错误
修复过程描述:(代码+上网,用时10mins,涉及单个函数)
运行测试,输出错误是java.lang.ArrayStoreException,通过网上搜索,获悉由于给数组赋值了不兼容的类型数据导致的,然后我将代码中用到的数组的类型和值类型输出,发现确实不同。根据函数的名称以及程序代码可以知道该函数是一个采样函数,在给定的一个list中采样,而数组的类型是采用了list中第一个元素的类型。再次运行输出list表中的所有元素的类型发现其中的数据类型并不一致。而采样函数又不能确定选择的是哪些元素,因此为了兼容性,直接选择了Object类型作为数组的类型,就可以接受任何类型的数据了。修复如下,正确通过所有测试。
// 修复之前代码final T[]out = (T[]) java.lang.reflect.Array.newInstance(singletons.get(0).getClass(), sampleSize);// 修复之后代码final T[]out = (T[]) java.lang.reflect.Array.newInstance(Object.class, sampleSize);标准patch:
- public Object[] sample(int sampleSize) throws NotStrictlyPositiveException {+ public T[] sample(int sampleSize) throws NotStrictlyPositiveException { if (sampleSize <= 0) { throw new NotStrictlyPositiveException(LocalizedFormats.NUMBER_OF_SAMPLES, sampleSize); } - final Object[] out = new Object[sampleSize];+ final T[]out = (T[]) java.lang.reflect.Array.newInstance(singletons.get(0).getClass(), sampleSize);
bug说明:求直线的反向直线精度损失
修复过程描述:(代码+注释,用时30mins,涉及单个函数)
根据测试输出很容易发现在求直线的反向直线的时候精度损失造成测试失败。原始的代码是根据现有直线的方向和zero向量相减求取逆向向量,在此过程中损失了精度,最直接的办法就是复制当前的直线然后改变其方向。但是原始程序中涉及到与zero向量的计算过程,其计算的结果会对原始的数据产生什么样的影响是未知的。因此通过数学化简的方式模拟其计算的过程(其中用了一些函数是通过注释信息了解其功能),发现计算之后zero的值是保持不变的。因此很容易修改成下面的代码,正确通过所有测试。
public Line revert() { //原始代码,下面的两行代码为修复代码//final Line reverted = new Line(zero, zero.subtract(direction)); final Line reverted = new Line(this); reverted.direction = direction.negate(); return reverted;}标准patch:
public Line revert() {- final Line reverted = new Line(this);- reverted.direction = reverted.direction.negate();+ final Line reverted = new Line(zero, zero.subtract(direction)); return reverted; }
bug说明:对于反正切函数Atan2(double,double)输入的参数是正负0时不能输出正确结果。
修复过程描述:(源代码+上网,用时2h,涉及多个函数)
运行测试发现当对Atan2(double,double)函数做特殊的输入(+0,-0)进行测试时未输出正确的结果。由于不是crash错误,因此只能单步跟踪调试,找到当输入的参数是0时的特殊处理办法。但是调试过程中并没有发现对0的特殊判断,调试了很长时间都没有确定能修改的位置。最后考虑到输入是0或Integer.MAX_INT等时,通常我们在写程序的时候也可能会单独做判断,因此只能采用打表的方式进行单独判断。然而对于正负0的判断并不知道如何解决,通过上网查找维基百科中有1/+0=+Infinite, 1/-0=-Infinite,采用这种方法对正负0进行判断。修复方案如下,可以正确通过所有测试。
...rootN(tmp2, 0, 2, tmp1, 0); // r = sqrt(x^2 + y^2)// 以下是修复的代码if(y[yOffset] == 0 && x[xOffset] == 0){ if(1.0 / y[yOffset] < 0){ if(1.0 / x[xOffset] < 0){ result[resultOffset] = - FastMath.PI; } else { result[resultOffset] = - 0.0; } } else { if(1.0 / x[xOffset] < 0){ result[resultOffset] = FastMath.PI; } else { result[resultOffset] = + 0.0; } } return;}// 以上是修复的代码if (x[xOffset] >= 0) { ...标准patch:
// fix value to take special cases (+0/+0, +0/-0, -0/+0, -0/-0, +/-infinity) correctly- result[resultOffset] = FastMath.atan2(y[yOffset], x[xOffset]); }
null(2)对该变量解引用出现NullPointerExceptionbug说明:给数组变量添加元素之后获取数组中元素的数量不变
修复过程描述:(代码,用时5分钟,涉及单个函数)
根据测试函数,可以看出采用的是增量测试,最开始其中的元素数量是0,然后向数组中添加元素,然后测试元素的数量是否更新。最初的想法是在添加元素的时候可能会触发一些变量的更新,单步调试发现并不是在添加元素的时候,而是在获取元素的数量时重新计算。在计算的函数中最开始是对一些特殊的条件的判断,比如没有plot对象时,直接返回为空数组,接下来很容易发现的错误是当plot对象不是null,并且其保存的dataset变量也不是null的时候依然返回了空数组,不合逻辑,因为根据测试代码,在dataset中添加元素,返回统计的信息应该是更新的,与这里直接返回空数组不符合,修改很简单,如下所示。正确通过所有测试。
//if (dataset != null) { //修改之前if (dataset == null) { //修改之后 return result;}标准patch:
CategoryDataset dataset = this.plot.getDataset(index);- if (dataset == null) {+ if (dataset != null) { return result; }
NaN时,maximum和minimum不可以是NaN,应该是其中的数据(2)根据另外两个测试输入,maxmnum和minimum的值可以来自数组中任何一位数据bug说明:求区间的最大和最小值时,最大和最小值两个标志量没有初始化,同时每次比较的时候只比较最大或最小,比较不完全,还有另一个数据没有加入考虑
修复过程描述:(源代码,用时30mins,涉及单个函数)
运行测试根据获得的最大和最小值不正确,很容易定位到求取区间最大、最小值的函数上。阅读代码以及结合当输入的参数是[1.0,NaN,NaN]时返回值是null,单步调试可知此过程中minimun和maximun两个变量在计算过程中没有被赋值,原因是比较的值是参数List中的第二和第三个参数,而期望的返回值是1.0,和List的第一个参数相同,因此想到当List存在数据时应该使用List中的数据首先初始化一下,而因为这个初始化应该放在循环中,需要只能初始化一次,故需要添加判断条件来确定只初始化一次,因此生成的修复如下代码:
if(minimum == Double.POSITIVE_INFINITY && maximum == Double.NEGATIVE_INFINITY){ minimum = maximum = intervalXYData.getXValue(series, item);}但是在运行其他的测试的时候发现,当输入的参数为[NaN,xx,xx]时,输出的结果也是不对的。再次单步跟踪调试发现上面的修复是minimum和maximum的值为NaN,在比较的时候不能更新,因此继续添加限定条件如下修复:
if(minimum == Double.POSITIVE_INFINITY && maximum == Double.NEGATIVE_INFINITY && !Double.isNaN(intervalXYData.getXValue(series, item))){ minimum = maximum = intervalXYData.getXValue(series, item);}修复之后之前的测试通过了,但是还有测试未能通过。当测试输入的参数是[1.0,NaN,0.5]时,期望的最大值和最小值分别是1.0和0.5,但是原来的代码中最大值只会和第三个数比,最小值只会和第二个数比,因此返回的最大和最小值都是1.0。所以,根据要求知道,最小值也会和第三位的数比较,同理最大数应该也和第二位的数比较最后的修复如下,所有测试通过。
for (int item = 0; item < itemCount; item++) { //以下为修复代码 if(minimum == Double.POSITIVE_INFINITY && maximum == Double.NEGATIVE_INFINITY && !Double.isNaN(intervalXYData.getXValue(series, item))){ minimum = maximum = intervalXYData.getXValue(series, item); } //以上为修复代码 lvalue = intervalXYData.getStartXValue(series, item); uvalue = intervalXYData.getEndXValue(series, item); if (!Double.isNaN(lvalue)) { minimum = Math.min(minimum, lvalue); maximum = Math.max(maximum, lvalue); //此行为修复代码 } if (!Double.isNaN(uvalue)) { minimum = Math.min(minimum, uvalue); //此行为修复代码 maximum = Math.max(maximum, uvalue); }}标准patch:
-752,19 +752,12 public final class DatasetUtilities { for (int series = 0; series < seriesCount; series++) { int itemCount = dataset.getItemCount(series); for (int item = 0; item < itemCount; item++) {- double value = intervalXYData.getXValue(series, item); lvalue = intervalXYData.getStartXValue(series, item); uvalue = intervalXYData.getEndXValue(series, item);- if (!Double.isNaN(value)) {- minimum = Math.min(minimum, value);- maximum = Math.max(maximum, value);- } if (!Double.isNaN(lvalue)) { minimum = Math.min(minimum, lvalue);- maximum = Math.max(maximum, lvalue); } if (!Double.isNaN(uvalue)) {- minimum = Math.min(minimum, uvalue); maximum = Math.max(maximum, uvalue); } } -1246,19 +1239,12 public final class DatasetUtilities { for (int series = 0; series < seriesCount; series++) { int itemCount = dataset.getItemCount(series); for (int item = 0; item < itemCount; item++) {- double value = ixyd.getYValue(series, item); double lvalue = ixyd.getStartYValue(series, item); double uvalue = ixyd.getEndYValue(series, item);- if (!Double.isNaN(value)) {- minimum = Math.min(minimum, value);- maximum = Math.max(maximum, value);- } if (!Double.isNaN(lvalue)) { minimum = Math.min(minimum, lvalue);- maximum = Math.max(maximum, lvalue); } if (!Double.isNaN(uvalue)) {- minimum = Math.min(minimum, uvalue); maximum = Math.max(maximum, uvalue); } }
maxY和minY是由对象中的数组元素决定的(2)克隆之后数组元素被重新初始化但是maxY和minY没有变化bug说明:TimeSeries复制之后最大值没有更新
修复过程描述:(源代码,用时10mins,涉及单个函数)
运行出错函数发现原始的对象中的最大值是102,但是只复制了其中部分元素,最大值不包含在里边,正确的答案应该是101。单步调试寻找对象最大值和最小值的更新位置,发现在每次添加元素的时候会重新计算,因此猜测复制之后的对象没有调用更新函数。继续单步调试,发现运行了更新函数,但是最大值没有变,由于是复制过去的,因此其最大值保持着原来的最大值102,更新之后还是最大值。因此想到,当复制元素的时候应该将这些标志量重新初始化。因此在复制函数中,当完成复制还没有添加元素的时候首先初始化这些最大最小值。修复方法如下:
TimeSeries copy = (TimeSeries) super.clone();copy.maxY = Double.NaN; //此行为修复代码copy.minY = Double.NaN; //此行为修复代码copy.data = new java.util.ArrayList();标准patch:
TimeSeries copy = (TimeSeries) super.clone();- copy.minY = Double.NaN;- copy.maxY = Double.NaN; copy.data = new java.util.ArrayList();
null(2)解引用导致`NullPointerExceptionbug说明:NullPointerException
修复过程描述:(代码,用时5mins,涉及单个函数)
由于是NullPointerException异常,属于crash错误,单独运行出错的测试程序,可以直接定位到出错的语句,找到出现空指针错误的变量。根据提示,出现空指针,是因为没有对其是否为null做判断就直接调用了对象的函数。最简单的方法就是对该变量的引用添加判断条件,判断其是否为null,当其不是null是执行相关的操作,当是null时,根据下面的代码发现不会影响继续执行,但是否应该做其他的处理还推测不出来。所以首先采用的是简单的修改方案,直接添加一个空指针判断。代码如下,结果再运行测试顺利通过,至于其为null时是否应该再做其他的操作不是很清楚。
if(r != null){ //此判断条件为修复内容,根据之后的代码,如果r==null,对includedAnnotations的引用不会造成影响,之后是对includedAnnotations中元素的遍历,因此可以直接修复 Collection c = r.getAnnotations(); Iterator i = c.iterator(); while (i.hasNext()) { XYAnnotation a = (XYAnnotation) i.next(); if (a instanceof XYAnnotationBoundsInfo) { includedAnnotations.add(a); } }}标准patch:
} - if (r != null) { Collection c = r.getAnnotations(); Iterator i = c.iterator(); while (i.hasNext()) { -4499,7 +4498,6 public class XYPlot extends Plot implements ValueAxisPlot, Pannable, includedAnnotations.add(a); } }- } }
index应该是负数(3)实际index是正数导致异常bug说明:IndexOutOfBoundException
修复过程描述:(代码+注释,用时5mins,涉及单个函数)
根据运行测试的输出是IndexOutOfBoundException,属于crash错误,根据输出的stacktrace很容易定位到错误的具体位置。发现是在往List中添加元素时传入了一个负值导致错误。然后根据上面的注释信息(negative index),当到达此处的时候 index 应该是负数,因此修复如下代码,通过测试,注释信息参考如下。
// if the series is sorted, the negative index is a result from// Collections.binarySearch() and tells us where to insert the// new item...otherwise it will be just -1 and we should just// append the value to the list...// if (this.autoSort) { //修复之前代码if (this.autoSort && index < 0) { // 修复之后代码 this.data.add(-index - 1, new XYDataItem(x, y));}标准patch:
if (x == null) { throw new IllegalArgumentException("Null 'x' argument."); }- if (this.allowDuplicateXValues) {- add(x, y);- return null;- } // if we get to here, we know that duplicate X values are not permitted XYDataItem overwritten = null; int index = indexOf(x);- if (index >= 0) {+ if (index >= 0 && !this.allowDuplicateXValues) { XYDataItem existing = (XYDataItem) this.data.get(index); try { overwritten = (XYDataItem) existing.clone();
Object的equals()方法(2)判断的类型是lib中的类,不能修改其equals()函数(3)期望的结果是判断两个对象的值是否相同bug说明:复制之后的对象和原来的对象不相等
修复过程描述:(代码+注释,用时30mins,涉及多个函数)
根据代码和测试结果,复制之后和复制之前不同。首先确定不相等的原因是什么,最开始怀疑的是复制的结果和原来的结果就是不一样的,即复制过程存在问题。因此开始单步调试复制的过程,结果发现过程比较繁琐,而且涉及到的都是一些系统的函数,比较难调试和观察。因此换做一种简单的方式,不看复制的过程,直接观察复制之后的对象和原来的对象,人工对比其对应的变量,结果发现却是一样的,但是判断的结果是两个对象不一致,因此应该是equals()函数存在错误。手动添加了一行l1.equals(l2)代码,作为调试使用。然后再单步调试。当比较两个Line2D$Double对象的时候最终判断结果跳转到了Object.equals()方法,根据注释信息,只有o1==o2的时候才会返回true,同时又由于Object的代码是不能修改的,所以问题不在这。但是根据调试的过程中调用的是Line2D.equals()函数,却跳转到了Object.equals()函数,说明Line2D类没有重写该函数,所以修复方法就是为Line2D类添加一个equals()函数。又发现Line2D也是系统库中的类,不能修改。所以只能再往上一层追溯对象比较的位置,最终发现在调用不同类的equals()函数前都是由一个ObjectUtilities.equals()函数进行分发的,因为对象的本身代码不能修改,只能在外边进行特殊判断了。修改代码如下,顺利通过测试。
public static boolean equal(final Object o1, final Object o2) { if (o1 == o2) { return true; } // 修改的代码从此以下,为Line2D类型添加单独的判断 if(o1 == null){ return o2 == null; } if(o1 instanceof Line2D){ if( o2 instanceof Line2D ){ Line2D line1 = (Line2D) o1; Line2D line2 = (Line2D) o2; return line1.getX1() == line2.getX1() && line1.getX2() == line2.getX2() && line1.getY1() == line2.getY1() && line1.getY2() == line2.getY2(); } return false; } return o1.equals(o2); // 下面的注释内容是修改之前的代码,修改的代码从此以上 // if (o1 != null) { // return o1.equals(o2); // } // else { // return false; // }}标准patch:
if (!(obj instanceof ShapeList)) { return false; }- ShapeList that = (ShapeList) obj;- int listSize = size();- for (int i = 0; i < listSize; i++) {- if (!ShapeUtilities.equal((Shape) get(i), (Shape) that.get(i))) {- return false;- }- }- return true;+ return super.equals(obj);
maxMiddleIndex的函数中,在计算minMiddleIndex时只用到了下标minMiddleIndex(2)在计算maxMiddleIndex的时候只用到了minMiddleIndexbug说明:在更新对象的元素的时候,对象的MaxMiddleIndex没有正确更新
修复过程说明:(源代码,用时10mins,涉及单个函数)
根据错误的测试,是在添加了元素之后获取对象的MaxMiddleIndex不是期望值。首先的反应是获取该值的时候计算出错getMaxMiddleIndex(),进入函数发现直接返回了对象的变量值,没有计算过程。然后观察测试函数只调用了一个add()方法,因此判断计算过程应该由该函数引发,错误应该在该函数内或者内部调用的其他的函数。进入后发现其中调用了两个函数,一个是updateBounds(),另一个是fireSeriesChanged()。根据名称两个函数都有可能。按顺序进入第一个函数updateBounds(),里边计算的都是关于max...和min...的变量,其中就有minMiddleIndex和maxMiddleIndex的计算过程,两个过程紧邻,除了一些计算的细微差别之外其他之处几乎一样,对比两个计算过程非常容易发现,在计算minMiddleIndex的时候只用到了minMiddleIndex,而计算maxMiddleIndex的时候没有用自己反而是用了minMiddleIndex,根据两个变量的相似性很有可能是不小心代码敲错了。将里边的minMiddleIndex都改成maxMiddleIndex后运行测试,成功通过。
if (this.maxMiddleIndex >= 0) { long s = getDataItem(this.maxMiddleIndex).getPeriod().getStart()//修复之前是minMiddleIndex .getTime(); long e = getDataItem(this.maxMiddleIndex).getPeriod().getEnd() //修复之前是minMiddleIndex .getTime(); long maxMiddle = s + (e - s) / 2; if (middle > maxMiddle) { this.maxMiddleIndex = index; }}标准patch:
if (this.maxMiddleIndex >= 0) {- long s = getDataItem(this.maxMiddleIndex).getPeriod().getStart()+ long s = getDataItem(this.minMiddleIndex).getPeriod().getStart() .getTime();- long e = getDataItem(this.maxMiddleIndex).getPeriod().getEnd()+ long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd() .getTime();
和Lang-8相似,参数没有被使用,违反了通常的使用模式。
public Week(Date time, TimeZone zone) {- this(time, zone, Locale.getDefault());+ this(time, RegularTimePeriod.DEFAULT_TIME_ZONE, Locale.getDefault()); }标准patch:
public Week(Date time, TimeZone zone) { // defer argument checking...- this(time, zone, Locale.getDefault());+ this(time, RegularTimePeriod.DEFAULT_TIME_ZONE, Locale.getDefault()); }
index传入另一个时间计算的函数中导致异常(3)获取上下界的函数中存在标志量emptyRange用来标识截取的区间是否为空bug说明:对TimeSeries进行截取区间时上下界不合法
修复过程描述:(代码,用时20mins,涉及单个函数)
运行错误的测试,发现出错的原因是传入的两个参数startIndex和endIndex不满足大小关系,根据此,逆向寻找传入参数的由来。在复制函数createCopy()函数中(根据代码似乎是复制一个时间对象,其时间区间给定),首先对start和end做了大小比较检查,然后在想要复制的对象中进行搜索,当搜索返回的是负数时对其进行+1后取反处理,不同的是endIndex取反之后比startIndex小了,导致传入另一个复制函数中出现异常。进入另一个函数发现其对参数的检查是合理的,endIndex不应该小于startIndex,否则接下来的循环不能执行。因此确定出错的位置是在调用该函数之前,即搜索之后的检查。根据代码,有一个标志是用来标注是否复制的内容为空的变量empyRange,当startIndex或endIndex小于0的时候该标识为true。现在的情况是传入的参数想要复制的时间段是合理的,即起始时间和结束时间满足先后顺序,但是都不在想要复制的对象中,此时应该返回一个空的区间,而不是抛出异常。依据此,生成如下的修复代码,通过测试。
//以下代码为修复代码if(startIndex > endIndex){ emptyRange = true;}//以上代码为修复代码if (emptyRange) { TimeSeries copy = (TimeSeries) super.clone(); copy.data = new java.util.ArrayList(); return copy;}标准patch:
}- if ((endIndex < 0) || (endIndex < startIndex)) {+ if (endIndex < 0) { emptyRange = true;
ToolTip信息的时候期望输出的结果是字符串中的\"转换为"bug说明:生成URL字符串不满足要求
修复过程描述:(代码,用时2mins,涉及单个函数)
根据出错测试函数定位到生成ToolTip的一个函数存在问题,阅读代码发现其将字符串转换为Html字符串时,\"字符期望转换的结果是";,进入转换函数内部发现其转换的过程非常简单,就是将传入的字符串转换添加了前缀和后缀,对传入的字符串并没有做解析。根据测试的输出要求,很容易修改成如下的代码,通过测试。
public String generateToolTipFragment(String toolTipText) { toolTipText = toolTipText.replaceAll("\\\"", """);//此行代码是添加代码,将\"替换成" return " title=\"" + toolTipText + "\" alt=\"\"";}标准patch:
public String generateToolTipFragment(String toolTipText) {- return " title=\"" + ImageMapUtilities.htmlEscape(toolTipText) + return " title=\"" + toolTipText + "\" alt=\"\""; }
bug说明:对JavaScript代码进行编译之后删除了没用的函数参数列表
修复过程描述:(代码+注释,用时5h20mins,涉及多个函数)
根据运行测试的结果,对于代码window.f = function(a) {};进行编译后,获得的语法树丢失了参数a的信息,猜测应该是在编译过程中对语法树的构建存在错误。由于并不是crash错误,不能定位具体的出错位置,因此只能使用单步调试的方法找到可能出错的具体位置。调试采用由粗粒度到细粒度逐步精化,缩小搜索空间的策略。首先对编译后的代码的AST树进行人工对比,但是由于树的结果太大,进行两个AST树的各个节点比较花费了很长时间,但是太乱没有找到问题。因为AST树的结构比较复杂,最后是比较两个AST树是否相等的,具体是哪里不相同应该先确定,然后再寻找构建AST树的过程中针对该部分的解析。通过单步调试,发现最后的比较只比较树中的first和next两个域的值,然后再次比较两个AST的结构,这次只关注上面的两个域,进行比较,然后在纸上画出两个AST树的结构,发现在参数部分编译之后的AST的first域为null,而实际上应该是存在一个参数a。根据测试代码,确定出错位置在compile的过程,然后单步调试此过程。在调试的过程中,根据之前画下的AST结果,每运行一条语句都观察一下最后保存AST的变量的值的变化,如果运行某条语句后该变量的对应位置被修改了,再对这条语句进行细化跟踪(这是后来采用的调试策略,开始调试的时候过于细致,导致浪费了很长时间,中途放弃采用这种调试方式)。最后确定了出错的位置是最后的optimize函数,想到编译优化的内容,删除无用的变量。然后单步调试优化的过程,同样采用上面的策略,最后定位到了traverseAndRemoveUnusedReferences(Node)函数,但是该函数中调用了两个值得怀疑的其他的函数removeUnreferencedVars()和removeUnreferencedFunctionArgs(),根据函数的名称,最先怀疑的是第二个函数,同时根据调试也是因为第二个函数的执行导致参数被删除。进入函数内部阅读详细的代码没有发现任何问题,不知道该如何修改。以为在对编译优化的设置上会有标志量,进入option的设置项中发现里边的标志量非常多,但是通过名称没有发现和函数的参数的优化相关的标志量。因此怀疑是之前的步骤不应该将变量a添加到无用变量列表中,接着单步调试之前的代码,同样没有发现。最后选择详细阅读代码以及代码注释信息,其中在函数removeUnreferencedFunctionArgs()中有这样的注释信息:
// Notice that removing unreferenced function args breaks// Function.prototype.length. In advanced mode, we don't really care// about this: we consider "length" the equivalent of reflecting on// the function's lexical source.//// Rather than create a new option for this, we assume that if the user// is removing globals, then it's OK to remove unused function args.根据上面的注释信息可以发现,作者认为“如果用户删除全局变量,那么就可以删除函数的无用参数”,阅读代码根据名称获知标志量removeGlobals是用来标志是否删除全局变量的。因此修复如下,成功通过测试。
if(!removeGlobals){ return;}标准patch:
// See http://code.google.com/p/closure-compiler/issues/detail?id=253- if (!removeGlobals) {- return;- }
null(2)解引用导致NullPointerException(3)当变量为null时,接下来的处理是没有意义的(对变量的域进行遍历)(4)函数没有返回值bug说明:NullPointerException
修复过程描述:(代码,用时5mins,涉及单个函数)
因为是crash错误,根据stack trace信息直接定位到出错的语句,观察代码发现是代码对函数调用返回的结果没有做null检查直接引用导致错误。然而根据接下来的代码发现,当返回的对象是null时,下面的代码等同于没有计算,是对其元素的遍历,所以简单修复直接就返回就好了,运行测试程序,成功通过。代码如下:
if(implicitProto == null){ //该条件语句为修复补丁 return;}// This can be the case if interfaceType is proxy to a non-existent// object (which is a bad type annotation, but shouldn't crash).currentPropertyNames = implicitProto.getOwnPropertyNames();标准patch:
Set<String> currentPropertyNames;- if (implicitProto == null) { // This can be the case if interfaceType is proxy to a non-existent // object (which is a bad type annotation, but shouldn't crash).- currentPropertyNames = ImmutableSet.of();- } else { currentPropertyNames = implicitProto.getOwnPropertyNames();- }
inline变量导致catch语句块中的变量在catch语句块之外被引用(2)catch语句中的变量不应该被inline(3)被inline的变量依赖catch语句块中的变量bug说明:在对代码编译优化时错误的inline了变量
修复过程描述:(源代码+注释,用时3.5h,涉及多个函数)
根据测试运行结果,非crash错误,需要单步调试定位错误的位置。使用由粗粒度到细粒度的调试方法逐步过滤,最后锁定到了FlowSensitiveInlineVariables类中的enterScope函数,该函数的作用是将一些可以inline的变量实现inline。然后对该部分代码进行单步跟踪调试,在调试的过程中基本理解了其大体的过程,首先是收集程序中的所有变量,然后通过正向和逆向的数据流分析过滤出可能会被inline的变量,同时也会分析变量的依赖关系,并且程序还会收集一些特殊的变量,比如在catch语句中的变量会被过滤掉。最后再将要inline之前,首先对变量的各种特殊情况进行判断,比如是不是函数的参数、是否依赖已经被inline的变量等。通过调试以及观察判断过程中保存的数据信息,发现针对下面的测试:
var a;try{ throw Error("");} catch(err){ a = err;}return a.stack;其中变量a依赖于err,而return语句已经超出了变量err的作用域,而代码中并没有发现对此进行判断。因此正确的修复应该添加对此判断。而调试的过程中发现其作用域Scope都是最大的域:整个函数(或许判断了但是由于相同没有排除掉)。所以通过作用域似乎不可行。因此采用了另一种比较取巧的方式。之前说到程序对catch做了特殊处理,所以这里我也采用了同样的处理方式,就是如果变量依赖于catch语句中的变量就不允许被inline,有点取巧,但是测试正确通过。修复代码如下:
//在函数canInline()中添加一条特殊判断for(Var dependency : defMetadata.depends){ if(dependency.getParentNode().isCatch()){ return false; }}标准patch:
for (Candidate c : candidates) {- if (c.canInline(t.getScope())) {+ if (c.canInline()) { c.inlineVariable(); // If definition c has dependencies, then inlining it may have -277,7 +277,7 class FlowSensitiveInlineVariables extends AbstractPostOrderCallback return defMetadata.node; } - private boolean canInline(final Scope scope) {+ private boolean canInline() { // Cannot inline a parameter. if (getDefCfgNode().isFunction()) { return false; -372,12 +372,6 class FlowSensitiveInlineVariables extends AbstractPostOrderCallback case Token.REGEXP: case Token.NEW: return true;- case Token.NAME:- Var var = scope.getOwnSlot(input.getString());- if (var != null- && var.getParentNode().isCatch()) {- return true;- } }
bug说明:在比较两个类型是否有继承/实现关系的时候出现死环
修复过程描述:(代码+注释,用时2h,涉及单个函数)失败
根据错误提示信息属于栈溢出,根据经验多数出现在递归调用出现缺少退出条件判断的情况,通过调试定位出错的位置发现在类型检查的过程中判断两个类型是否具有继承/实现关系的时候,获取类型实现的接口时返回的是自己本身,导致循环调用栈溢出。所以想到的修改方案是将获取到类实现的接口和自己比较,如果实现的接口和自己相同,即形成环则退出,不再进行递归。修复代码如下:
//原始代码if (thatCtor != null && thatCtor.isInterface()) { Iterable<ObjectType> thisInterfaces = getCtorImplementedInterfaces(); for (ObjectType thisInterface : thisInterfaces) { if (thisInterface.isSubtype(that)) { return true; } }}//修复之后的代码if (thatCtor != null && thatCtor.isInterface()) { Iterable<ObjectType> thisInterfaces = getCtorImplementedInterfaces(); for (ObjectType thisInterface : thisInterfaces) { if (!this.isEquivalentTo(thisInterface) && thisInterface.isSubtype(that)) { return true; } }}然后再次运行刚刚的测试,发现没有了栈溢出错误,但是还存在错误。根据测试的注释信息,该测试是期望出现解析错误,识别出有继承环Parse error. Cycle detected in inheritance chain of type MyType。根据提示的错误信息进行单步调试试图发现对实现的接口的处理,但是当处理annotation信息时并不知道当前的具体类是什么,所以没有定位到具体应该在哪里修复,修复失败。
标准patch:
+++ b/src/com/google/javascript/rhino/jstype/NamedType.java -187,7 +187,7 class NamedType extends ProxyObjectType { // makes more sense. Now, resolution via registry is first in order to // avoid triggering the warnings built into the resolution via properties. boolean resolved = resolveViaRegistry(t, enclosing);- if (detectInheritanceCycle()) {+ if (detectImplicitPrototypeCycle()) { handleTypeCycle(t); } -199,7 +199,7 class NamedType extends ProxyObjectType { } resolveViaProperties(t, enclosing);- if (detectInheritanceCycle()) {+ if (detectImplicitPrototypeCycle()) { handleTypeCycle(t); }
inline的变量是其中一个对象的属性(3)语句中存在删除该属性的操作bug说明:删除一个变量和删除变量的属性是不等价的,存在变量属性不能被inline
修复过程描述:(代码+注释,用时2h,涉及多个函数)
根据测试的输出信息以及测试函数的名称获知,源代码中的变量不应该被inline。通过debug过程定位出错的函数应该出在InlineObjectLiterals类的afterExitScope函数。根据函数代码的命名情况,首先怀疑出错的函数可能是isVarInlineForbidden函数出错,该函数是判断当前的变量是否被忽略,即不被inline。进入该函数阅读代码发现,注释信息说明:当变量是作为输出(用于返回值)的不应该被inline,结合测试的代码中存在return语句怀疑错误就是因为没有对语句是否为return语句进行判断。因此想在该处进行判断,通过调试并输出程序状态发现该函数中的变量不包含完整的return语句,只是一个变量,因此不能判断是否被用于return语句中。返回afterExitScope函数,发现另一个值得怀疑的函数isInlineObject(),判断当前的变量是否被inline。同时注意到一点传入该函数的参数是对当前变量的引用信息。进入函数内部阅读代码,大体内容就是对变量的引用进行检查是否能使用变量进行替换。在此过程中获取到了变量的引用处的节点信息、父节点信息以及祖先节点信息。debug输出这些内容,发现能够获得其所在的return语句的完整结构。然后根据代码发现对其父节点和祖先节点的类型都有判断。其中注意到,当该变量在return语句中被引用时,其祖先节点是完整的return语句。结合代码对祖先节点的判断,生成如下的修复:当变量被引用的位置是return语句时,不被inline。
if (parent.isGetProp()) { Preconditions.checkState(parent.getFirstChild() == name); if ((gramps.isCall() || grams.isReturn()) //添加了是否是return语句的判断条件 && gramps.getFirstChild() == parent) { return false; }}运行当前的测试顺利通过,然后运行其他的测试发现出现了新的失败测试。因此怀疑刚刚的修复是否正确。又仔细阅读代码和注释信息,在注释中发现这样一句话:
// Deleting a property has different semantics from deleting// a variable, so deleted properties should not be inlined.删除变量的属性和删除变量是语义不等价的,而代码中对变量的引用中存在一处删除属性语句delete foo.bar。但是对刚刚的return语句就不考虑了吗?想想return的说明在另一个函数中,注释信息跨函数似乎不太可能,因此根据注释信息还原了上面的修改,生成新的修复代码如下:
// Deleting a property has different semantics from deleting// a variable, so deleted properties should not be inlined.if(gramps.isDelProp()){ return false;}再运行测试,成功通过。
标准patch:
// Deleting a property has different semantics from deleting // a variable, so deleted properties should not be inlined.- if (gramps.isDelProp()) {- return false;- }
warning(2)判断用于赋值的变量类型是否合法时调用了expectCanAssignTo()函数(3)未通过测试调用该函数返回值是`falsebug说明:变量的声明类型和赋值变量类型不一致没有report warnings
修复过程描述:(代码,用时3h,涉及多个函数)
根据输出的测试错误信息,结合测试的输入数据,发现错误的原因是变量的声明类型和赋值类型不一致,期望能够以warning的形式报告。因此单步调试找到对该语句中的变量类型进行检查的代码段。最后定位到TypeCheck类的visitVar函数,在该函数中是调用了validator.expectCanAssignTo()函数返回一个boolean值表示该赋值类型是否匹配,调试输出发现输出结果是false,因此猜测可以在此处判断是否满足赋值的类型一致性,如果返回false则报告warning,但是如何report?在该类的上下文中的其他函数中找到了一个report()函数,可以记录当前的warning信息。因此添加了一个report函数调用,修复代码如下:
//原程序validator.expectCanAssignTo( t, value, valueType, nameType, "initializing variable");//修复程序if(!validator.expectCanAssignTo( t, value, valueType, nameType, "initializing variable")){ report(t, n, DiagnosticType.warning(null, "Variable type inconsistant")); }结果再运行测试的时候发现抛出了NullPointerException,单步调试发现传入的DiagnosticType应该在事先定义好并添加到DiagnosticGroup中,否则在report的过程中会去查找是否存在该DiagnosticType的定义,因为没有定义,导致NullPointerException错误。因此,仿照之前代码的定义格式,自己定义了一个变量,代码如下,
static final DiagnosticType ILLEGAL_VAR_INITISLIZING = DiagnosticType.warning("JSC_ILLEGAL_VAR_INITIALIZING", "Variable type inconsistant");并将其加入到了All_DIAGNOSTICS中。再次运行测试时发现还是错的,原因是输出的warning信息内容不对。返回修复的代码处发现函数validator.expectCanAssignTo()函数传入的参数和输出的一致。所以判断应该是在该函数中修复,将上面的修复内容复原。进入validator.expectCanAssignTo()函数阅读代码发现其中调用的一个mismatch函数就是用来report warning的,而当其中判断条件if语句满足时只调用了registerMismatch函数,没有调mismatch。根据函数的名称前一个是注册,第二个应该是报告,所以在注册之后应该再报告。修复代码如下:
if ((leftType.isConstructor() || leftType.isEnumType()) && (rightType.isConstructor() || rightType.isEnumType())) { registerMismatch(rightType, leftType, null); mismatch(t, n, msg, rightType, leftType); //添加修复代码} else { mismatch(t, n, msg, rightType, leftType);}再运行刚刚的测试,顺利通过。但是存在其他的错误测试,发现是同样的问题,很快找到对应的函数validator.expectCanAssignToPropertyOf()函数,同样if语句中少调了mismatch函数,同样修复方法,顺利通过测试。
if ((leftType.isConstructor() || leftType.isEnumType()) && (rightType.isConstructor() || rightType.isEnumType())) { registerMismatch(rightType, leftType, null); mismatch(t, n, "assignment to property " + propName + " of " + //添加修复代码 getReadableJSTypeName(owner, true), rightType, leftType);} else { ... mismatch(t, n, "assignment to property " + propName + " of " + getReadableJSTypeName(owner, true), rightType, leftType);}标准patch:
if (!leftType.isNoType() && !rightType.canAssignTo(leftType)) {+ if ((leftType.isConstructor() || leftType.isEnumType()) && (rightType.isConstructor() || rightType.isEnumType())) {+ registerMismatch(rightType, leftType, null);+ } else { // Do not type-check interface methods, because we expect that // they will have dummy implementations that do not match the type // annotations. -379,6 +382,7 class TypeValidator { "assignment to property " + propName + " of " + getReadableJSTypeName(owner, true), rightType, leftType);+ } return false; } return true; -398,7 +402,11 class TypeValidator { boolean expectCanAssignTo(NodeTraversal t, Node n, JSType rightType, JSType leftType, String msg) { if (!rightType.canAssignTo(leftType)) {+ if ((leftType.isConstructor() || leftType.isEnumType()) && (rightType.isConstructor() || rightType.isEnumType())) {+ registerMismatch(rightType, leftType, null);+ } else { mismatch(t, n, msg, rightType, leftType);+ } return false;
Object|boolean|number|string期望的解析结果是不变(2)实际运行删除了Object类型(3)调试发现该Object缺失是由于函数caseObjectType()的返回值为null导致的(3)根据注释Object可能是function的子类,当约束Object为Function时(存在一个标志量),保留子类,否则过滤掉function的子类bug说明:对于类型的过滤,Object类型存在两种,一种是实现了Call方法的,另一种是没有实现Call方法的,在过滤类型时需要对其进行特殊考虑
修复过程描述:(源代码+注释,用时2h,涉及单个函数)
根据测试输出结果,其输入的类型是Object|boolean|number|string,期望输出的结果也是Object|boolean|number|string,而实际输出的结果却是boolean|number|string,导致assert错误。通过调试发现代码对每一个类型都会进行单独处理,而处理的函数却不一样。根据上面的输出只需要对Object类型的处理进行调试即可。单步调试发现最终决定是否保留该Object类型是根据函数caseObjectType()的返回值。其中涉及到一系列的条件判断。代码如下:
public JSType caseObjectType(ObjectType type) { if (value.equals("function")) { JSType ctorType = getNativeType(U2U_CONSTRUCTOR_TYPE); return resultEqualsValue && ctorType.isSubtype(type) ? ctorType : null; // Objects are restricted to "Function", subtypes are left // Only filter out subtypes of "function" } return matchesExpectation("object") ? type : null;}单步调试发现该测试输入的type=Object,会进入到if的true分支,而且获取到的ctorType是Function,而当resultEqualsValue为true的时候该语句执行正确,可以通过测试,当其为false时导致过滤掉了Object类型,因此首先想到的是应该对resultEqualsValue进行分类处理,这样才不会影响之前的正确测试。因此修复的代码框架为:
public JSType caseObjectType(ObjectType type) { if (value.equals("function")) { JSType ctorType = getNativeType(U2U_CONSTRUCTOR_TYPE); // return resultEqualsValue && ctorType.isSubtype(type) ? ctorType : null; if(resultEqualsValue){ ctorType.isSubtype(type) ? ctorType : null; } else { ???? } // Objects are restricted to "Function", subtypes are left // Only filter out subtypes of "function" } return matchesExpectation("object") ? type : null;}上面的代码可以保证当resultEqualsValue=true时依然可以正常通过测试。但是上面的????处应该如何填写却成了难题。最后依照上面的注释信息,意思是如果Object被约束为“Function”时,保留其子类,只过滤掉“function”的子类。但是没有读懂到底是什么意思。然后就寻找"Function"和“function"的区别是什么。在调用的上一层函数中的注释中找到如下说明:
* Returns a version of { type} that is restricted by some knowledge * about the result of the { typeof} operation. * <p> * The behavior of the { typeof} operator can be summarized by the * following table: * <table> * <tr><th>type</th><th>result</th></tr> * <tr><td>{ undefined}</td><td>"undefined"</td></tr> * <tr><td>{ null}</td><td>"object"</td></tr> * <tr><td>{ boolean}</td><td>"boolean"</td></tr> * <tr><td>{ number}</td><td>"number"</td></tr> * <tr><td>{ string}</td><td>"string"</td></tr> * <tr><td>{ Object} (which doesn't implement [[Call]])</td> * <td>"object"</td></tr> * <tr><td>{ Object} (which implements [[Call]])</td> * <td>"function"</td></tr> * </table> * type the type to restrict * value A value known to be equal or not equal to the result of the * { typeof} operation * resultEqualsValue { true} if the { typeOf} result is known * to equal { value}; { false} if it is known <em>not</em> to * equal { value} * the restricted type or null if no version of the type matches the * restriction上面的注释信息详细的说明了各个参数的含义。因此得知,Object类型可以实现"Call",也可以不实现,但是两者是不一样的。再理解上面的两条注释,猜测上面的两句话应该是说的两种情况(因为连不到一起):情况一,当resultEqualsValue为true的时候保留其(‘function’)子类。情况二,当resultEqualsValue为false的时候过滤掉”function“的子类。再结合调试的时候的类型信息,修改上面的修复代码如下,通过测试。
public JSType caseObjectType(ObjectType type) { if (value.equals("function")) { JSType ctorType = getNativeType(U2U_CONSTRUCTOR_TYPE); // return resultEqualsValue && ctorType.isSubtype(type) ? ctorType : null; if(resultEqualsValue){ return ctorType.isSubtype(type) ? ctorType : null; } else { return type.isSubtype(ctorType) ? null : type; } // Objects are restricted to "Function", subtypes are left // Only filter out subtypes of "function" } return matchesExpectation("object") ? type : null;}标准patch:
JSType ctorType = getNativeType(U2U_CONSTRUCTOR_TYPE);- if (resultEqualsValue) {+ return resultEqualsValue && ctorType.isSubtype(type) ? ctorType : null; // Objects are restricted to "Function", subtypes are left- return ctorType.getGreatestSubtype(type);- } else { // Only filter out subtypes of "function"- return type.isSubtype(ctorType) ? null : type;- } }
bug说明:对变量声明进行合并优化时将函数的参数没有特殊考虑,进行声明合并导致二次声明
修复过程描述:(源代码,用时30mins,涉及单个函数)
根据测试输出的结果以及之前的调试经验,很快定位到对变量进行声明合并优化的函数。其过程分成两步,首先是搜集可以合并的变量,然后再执行合并的操作。根据测试很明显,函数的参数不应该出现在合并的列表中。因此错误应该处在搜集的过程。单步调试,发现在对变量进行判断是否能合并声明时进行了一些判断,但是当到函数内部的时候并不能获取当前的变量是否是在函数参数列表中。所以应该是有一个标志量来标志一些变量不能被合并声明。阅读代码发现在判断的过程中有一个集合保存的变量会被排除掉,不被合并。因此想到的方法就是在遍历到函数的参数列表时对参数变量进行标记,即添加到该集合中,在之后访问同样的变量时就不会被合并了。依据此,生成如下的修复代码,成功通过测试。
//添加修复代码if(n.isName() && n.getParent().isParamList()){ blacklistedVars.add(t.getScope().getVar(n.getString()));}标准patch:
&& var.getScope() == s- && !isNamedParameter(var) && !blacklistedVars.contains(var); } } - private boolean isNamedParameter(Var v) {- return v.getParentNode().isParamList();- }
/字符作为地址的分隔符(2)输入的地址可能使用\作为分隔符(2)程序没有考虑\分隔符导致出错bug说明:对字符串中的\\没有处理,只处理了/字符,导致Model名称解析错误
修复过程描述:(源程序+注释+上网,用时10mins,涉及单个函数)
根据测试输出发现对Model的前缀并没有处理。调试发现到函数normalizeSourceName中是将之前输入的前缀直接截取掉。而前缀中的分隔符采用的是/符号,根据注释说明:The DOS command shell will normalize "/" to "\", so we have to wrestle it back,因此很容易想到的修复代码如下:
filename = filename.replaceAll("\\", "/");再次运行测试结果在Regex编译的时候出错,上网查找原因是在java中字符串中的\字符需要转义,同时在Regex翻译过程中\需要再次转义,因此,想要匹配\字符需要两次转义,修复代码如下:
filename = filename.replaceAll("\\\\", "/");成功通过测试。
标准patch:
// The DOS command shell will normalize "/" to "\", so we have to // wrestle it back.- filename = filename.replace("\\", "/"); if (filename.indexOf(filenamePrefix) == 0) { filename = filename.substring(filenamePrefix.length()); -181,7 +180,7 public class ProcessCommonJSModules implements CompilerPass { Preconditions.checkArgument(scriptNodeCount == 1, "ProcessCommonJSModules supports only one invocation per " + "CompilerInput / script node");- String moduleName = guessCJSModuleName(script.getSourceFileName());+ String moduleName = guessCJSModuleName(normalizeSourceName(script.getSourceFileName())); script.addChildToFront(IR.var(IR.name(moduleName), IR.objectlit())
+不一定是数值运算,可能是字符串的连接运算(3)当此处的+是字符串的连接运算时不能进行预计算(4)当+的计算类型不确定时为了保证正确性不进行预计算bug说明:对JavaScript代码的加法进行优化时,如果中间计算步骤可能出现String类型则不能进行加法先计算
修复过程描述:(源代码+注释,用时40mins,涉及单个函数)
根据测试输出信息,源程序对编译的代码进行了优化,将加法操作提前进行运算。根据输出结果是不应该被优化的,但是根据输出没有理解为什么不能优化。因此单步调试尝试理解其整个过程,在调试的过程中根据注释信息发现加法操作可能不是数值的运算,也可能是字符串的连接运算,
// Left's right child MUST be a string. We would not want to fold// foo() + 2 + 'a' because we don't know what foo() will return, and// therefore we don't know if left is a string concat, or a numeric add.结合测试输入的数据,
var a =(Math.random()>0.5? '1' : 2 ) + 3 + 4;不能判定前部分返回的是字符串'1'还是数值2。找到了原因开始找程序的出错位置。在调试过程中发现每次进行优化之前都会进行判断运算的两个部分有没有可能是string类型。根据调试结果对于前部分的判断是不可能。这明显是错的。基本定位到出错的位置,在判断节点的类型时。判断其是否为string类型时调用了一个函数mayBeString(),根据函数名称知道只要可能就会返回true。而上面的前部分明显可能为string的。继续调试发现在该函数中调用了一个allResultMatch()函数,该函数会将节点进行分解判断是否每一子部分都可能是string类型,只有都是string的时候才会返回true。上面的节点判断时,'1'返回的是true,2返回的是false,使得最终结果是false。很明显这里不应该取合取运算。而刚好就在该函数下面有一个函数叫anyResultMatch()函数,根据注释和代码其判定当前节点子节点有一个可能为string类型就返回true,即返回子结构的析取运算。很容易修改代码如下,成功通过测试。
static boolean mayBeString(Node n, boolean recurse) { if (recurse) { // return allResultsMatch(n, MAY_BE_STRING_PREDICATE); //修改之前代码 return anyResultsMatch(n, MAY_BE_STRING_PREDICATE); //修改之后代码 } else { return mayBeStringHelper(n); }}标准patch:
if (recurse) {- return anyResultsMatch(n, MAY_BE_STRING_PREDICATE);+ return allResultsMatch(n, MAY_BE_STRING_PREDICATE); } else {
era类型在插入时间序列中被插入到year后面,违反单位从大到小排列(2)era类型的UnitMillis的值为0,导致和year比较时返回值为-1,实际期望的返回值为1(3)跟踪调试发现比较的过程中year和month等都有对应的DurationType,而era对应的却是UnsupportedDurationTypebug说明:对时间的单位不按顺序排序
修复过程描述:(源代码,用时30mins,涉及单个函数)
通过测试函数可以定位到函数出错的位置是compareTo()函数,比较两个时间单位的大小。根据之前运行的结果可以获得时间的排序是按照由大单位到小单位进行排列的(年,月,日)。而比较单位的大小关系是通过类的一个域unitMillis作为标记的。当获取世纪era的标记时发现返回的一直是0,其实际应该比年要大,结果却返回0.导致比较结果错误,即世纪小于年。因此猜测对于世纪可能需要特殊处理,因此对原来的比较函数进行修改如下:
long otherMillis = otherField.getUnitMillis();long thisMillis = getUnitMillis();// cannot do (thisMillis - otherMillis) as can overflowif (thisMillis == otherMillis) { return 0;}if (thisMillis < otherMillis || otherMillis == 0) { //修改方法,对0做特殊判断 return -1;} else { return 1;}然后运行刚刚的测试正确通过,但是导致其他的测试出现了错误,调试结果发现由于上面的修复导致。因此猜测上面的修复是错误的,但是观察到该测试获取到的otherMillis值也是0却正常通过了,了解到应该不是0的问题。然后通过调试比较两个测试在该步的不同点,发现原来的测试中世纪era在获取对应的DurationField时返回的是UnsupportedDurationField,根据名称猜测世纪本身在该比较单位中是不受支持的,因此针对上面的错误重新修改修复补丁如下:
if(otherField instanceof UnsupportedDurationField){ //修复方法,对不受支持的类型做特殊判断 return -1;}long otherMillis = otherField.getUnitMillis();long thisMillis = getUnitMillis();// cannot do (thisMillis - otherMillis) as can overflowif (thisMillis == otherMillis) { return 0;}if (thisMillis < otherMillis) { return -1;} else { return 1;}标准patch:
+++ b/src/main/java/org/joda/time/Partial.java -214,20 +214,11 public final class Partial DateTimeFieldType loopType = types[i]; DurationField loopUnitField = loopType.getDurationType().getField(iChronology); if (i > 0) {- if (loopUnitField.isSupported() == false) {- if (lastUnitField.isSupported()) {- throw new IllegalArgumentException("Types array must be in order largest-smallest: " +- types[i - 1].getName() + " < " + loopType.getName());- } else {- throw new IllegalArgumentException("Types array must not contain duplicate unsupported: " +- types[i - 1].getName() + " and " + loopType.getName());- }- } int compare = lastUnitField.compareTo(loopUnitField); if (compare < 0) { throw new IllegalArgumentException("Types array must be in order largest-smallest: " + types[i - 1].getName() + " < " + loopType.getName());- } else if (compare == 0 && lastUnitField.equals(loopUnitField)) {+ } else if (compare == 0) { if (types[i - 1].getRangeDurationType() == null) { if (loopType.getRangeDurationType() == null) { throw new IllegalArgumentException("Types array must not contain duplicate: " +diff --git a/src/main/java/org/joda/time/field/UnsupportedDurationField.java b/src/main/java/org/joda/time/field/UnsupportedDurationField.javaindex bf44e01..7e0ce57 100644--- a/src/main/java/org/joda/time/field/UnsupportedDurationField.java+++ b/src/main/java/org/joda/time/field/UnsupportedDurationField.java -224,6 +224,9 public final class UnsupportedDurationField extends DurationField implements Ser * zero always */ public int compareTo(DurationField durationField) {+ if (durationField.isSupported()) {+ return 1;+ } return 0;
era)和年(year)在获取RangeDurationType时的返回值都是null被判定为同一类型抛出异常(2)实际两个类型是不同的单位bug说明:世纪era和年year并列时由于获取getRangeDurationType都为null,导致错判为同一单位。
修复过程描述:(代码,用时20mins,涉及单个函数)
根据未通过测试,定位到exception被抛出的位置,单步调试发现在此处时由于获得era和year的RangeDurationType返回的都是null被错判为相同的类型。结合测试的assert信息,era应该正确的被插入到该Partial中,而且根据调试观察,其插入的位置也是正确的,因此判断就是在抛出异常的位置判断不够全面导致错误,原本不是一个类型但是其RangeDurationType信息都是null导致出错,修复方法是添加判断条件,代码如下,成功通过测试。
if (types[i - 1].getRangeDurationType() == null) { if (loopType.getRangeDurationType() == null) { //修复代码,当类型的名称一样时判断为同一类型 if(types[i-1].getName().equals(loopType.getName())){ throw new IllegalArgumentException("Types array must not contain duplicate: " + types[i - 1].getName() + " and " + loopType.getName()); } }}标准patch:
if (i > 0) { int compare = lastUnitField.compareTo(loopUnitField);- if (compare < 0) {+ if (compare < 0 || (compare != 0 && loopUnitField.isSupported() == false)) { throw new IllegalArgumentException("Types array must be in order largest-smallest: " + types[i - 1].getName() + " < " + loopType.getName()); } else if (compare == 0) { -446,9 +446,6 public final class Partial if (compare > 0) { break; } else if (compare == 0) {- if (fieldType.getRangeDurationType() == null) {- break;- } DurationField rangeField = fieldType.getRangeDurationType().getField(iChronology); DurationField loopRangeField = loopType.getRangeDurationType().getField(iChronology); if (rangeField.compareTo(loopRangeField) > 0) {diff --git a/src/main/java/org/joda/time/field/UnsupportedDurationField.java b/src/main/java/org/joda/time/field/UnsupportedDurationField.javaindex 7e0ce57..bf44e01 100644--- a/src/main/java/org/joda/time/field/UnsupportedDurationField.java+++ b/src/main/java/org/joda/time/field/UnsupportedDurationField.java -224,9 +224,6 public final class UnsupportedDurationField extends DurationField implements Ser * zero always */ public int compareTo(DurationField durationField) {- if (durationField.isSupported()) {- return 1;- } return 0;
add()中,当传入加的参数值为0的时候会由于做时间的调整导致出错(2)期望的结果是保持原来的时间不变bug说明:在原来的时间基础之上加上0时长会出现错误
修复过程描述:(代码+注释,用时15mins,涉及单个函数)
根据未通过测试发现当为日期加一个0时会出现与原来不相等,通常来说在计算的过程中应该一直保持原来的时间不变,因此单步调试发现在进行加法运算时,对原来的时间首先加了一个时长然后进行运算,考虑为什么需要添加这个时长呢?根据注释信息说明,
/* * Because time durations are typically smaller than time zone offsets, the * arithmetic methods subtract the original offset. This produces a more * expected behavior when crossing time zone offset transitions. For dates, * the new offset is subtracted off. This behavior, if applied to time * fields, can nullify or reverse an add when crossing a transition. */虽然并没有很理解,但似乎是有意义的,因此不对其原来的计算方式进行修改,而是对0做特殊的判断。修改如下,成功通过测试。
public long add(long instant, int value) { if(value == 0){ //修复代码,当加的时长是0时,直接退出,不再进行计算 return instant; } int offset = getOffsetToAdd(instant); instant = iField.add(instant + offset, value); return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant));}标准patch:
-636,9 +636,7 public class MutableDateTime if (type == null) { throw new IllegalArgumentException("Field must not be null"); }- if (amount != 0) { setMillis(type.getField(getChronology()).add(getMillis(), amount));- } } //----------------------------------------------------------------------- -659,9 +657,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addYears(final int years) {- if (years != 0) { setMillis(getChronology().years().add(getMillis(), years));- } } //----------------------------------------------------------------------- -682,9 +678,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addWeekyears(final int weekyears) {- if (weekyears != 0) { setMillis(getChronology().weekyears().add(getMillis(), weekyears));- } } //----------------------------------------------------------------------- -705,9 +699,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addMonths(final int months) {- if (months != 0) { setMillis(getChronology().months().add(getMillis(), months));- } } //----------------------------------------------------------------------- -728,9 +720,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addWeeks(final int weeks) {- if (weeks != 0) { setMillis(getChronology().weeks().add(getMillis(), weeks));- } } //----------------------------------------------------------------------- -771,9 +761,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addDays(final int days) {- if (days != 0) { setMillis(getChronology().days().add(getMillis(), days));- } } //----------------------------------------------------------------------- -794,9 +782,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addHours(final int hours) {- if (hours != 0) { setMillis(getChronology().hours().add(getMillis(), hours));- } } //----------------------------------------------------------------------- -827,9 +813,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addMinutes(final int minutes) {- if (minutes != 0) { setMillis(getChronology().minutes().add(getMillis(), minutes));- } } //----------------------------------------------------------------------- -860,9 +844,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addSeconds(final int seconds) {- if (seconds != 0) { setMillis(getChronology().seconds().add(getMillis(), seconds));- } } //----------------------------------------------------------------------- -895,9 +877,7 public class MutableDateTime * IllegalArgumentException if the value is invalid */ public void addMillis(final int millis) {- if (millis != 0) { setMillis(getChronology().millis().add(getMillis(), millis));- } }
hourOfDay,而传入的参数的类型是clockhourOfDay(2)两个类型表示的是同一个时间单位(3)两个类型在时间单位的表中的index不同(4)期望的结果是抛出IllegalArgumentExceptionbug说明:同样的计时单位具有不同的index导致更新的时候会出现重复错误
修复过程描述:(代码,用时15mins,涉及单个函数)
根据测试函数正确的运行结果应该是抛出异常,单步调试发现原来的计时单位中有hourOfDay,而传入的参数是clockhourOfDay,虽然指的是同一个时间单位,但是是不同的表示方法,导致其index不同,所以没有更新原来的时间而是重新添加了一个新的计时单位clockhourOfDay,产生重复。因此想到的是如果更新时间信息传入的参数要么和原来的计时单位是一致的,要么传入的参数在原来的计时单位中不存在,不能同一单位用不同的表示方法。因此修复方法就是检测是否存在表示的是同一计时单位却不是同一个类,修复代码如下,通过测试。
DurationField rangeField = fieldType.getRangeDurationType().getField(iChronology);DurationField loopRangeField = loopType.getRangeDurationType().getField(iChronology);if (rangeField.compareTo(loopRangeField) > 0) { break;}//修复代码,当两个计量单位表示的是同一时间单位时但是名称不同抛出异常if(rangeField.compareTo(loopRangeField) == 0){ throw new IllegalArgumentException("Two same types with different index.");}标准patch:
// this isn't overly efficient, but is safe- Partial newPartial = new Partial(newTypes, newValues, iChronology);+ Partial newPartial = new Partial(iChronology, newTypes, newValues);
UnsupportedOperationException异常bug说明:在将时间进行格式化的时候当不支持的单位出现时没有对其进行转换导致出错
修复过程描述:(源代码+注释,用时30mins,涉及单个函数)
根据测试函数,对时间进行标准化的时候原本的时间是按年,月,周计时的,但是标准化的格式要求没有年,而是使用月,周,天进行表示。通过单步调试发现在设置对应单位的数值之前并没有对该单位是否支持进行判断(比如该例子中对年是不支持的),从而导致异常出现。知道了这一点就很好修改了,只要在计算之前确定对应位是否受支持,然后有目标的进行转换就可以了。然后根据注释中的说明,如果由于类型不受支持导致最终的年份或月份不能完全被设置到period类型中,应该抛出UnsupportedOperationException异常。修改的代码如下:
if(years != 0 || months != 0){ if(result.isSupported(DurationFieldType.YEARS_TYPE)){ years = FieldUtils.safeAdd(years, months / 12); months = months % 12; if(years != 0) { result = result.withYears(years); } years = 0; } if(result.isSupported(DurationFieldType.MONTHS_TYPE)){ months = FieldUtils.safeAdd(months, years * 12); result = result.withMonths(months); months = 0; } if(months != 0){ throw new UnsupportedOperationException(); }}//以下注释部分为原始代码,上面为修复代码// if (years != 0 || months != 0) {// years = FieldUtils.safeAdd(years, months / 12);// months = months % 12;// if (years != 0) {// result = result.withYears(years);// }// if (months != 0) {// result = result.withMonths(months);// }// }return result;标准patch:
if (years != 0 || months != 0) {- long totalMonths = years * 12L + months;- if (type.isSupported(DurationFieldType.YEARS_TYPE)) {- int normalizedYears = FieldUtils.safeToInt(totalMonths / 12);- result = result.withYears(normalizedYears);- totalMonths = totalMonths - (normalizedYears * 12);+ years = FieldUtils.safeAdd(years, months / 12);+ months = months % 12;+ if (years != 0) {+ result = result.withYears(years); }- if (type.isSupported(DurationFieldType.MONTHS_TYPE)) {- int normalizedMonths = FieldUtils.safeToInt(totalMonths);- result = result.withMonths(normalizedMonths);- totalMonths = totalMonths - normalizedMonths;- }- if (totalMonths != 0) {- throw new UnsupportedOperationException("Unable to normalize as PeriodType is missing either years or months but period has a month/year amount: " + toString());+ if (months != 0) {+ result = result.withMonths(months); }
bug说明:时间相加时跨公元前后导致出错
修复过程描述:(源代码,用时2h,涉及多个函数)失败
根据测试的数据很容易发现当公元前的年份加上一个负数导致跨公元时由于没有对0做特殊调整,根据单步调试的信息,发现在加函数中plusYears()有对加的年份是否为0进行判断,其他的没有判断,猜测在此处进行特殊的判断,修复代码如下:
//以下是修复代码,当不同符号的年份进行运算时进行特殊处理if(((long) years * getYear()) < 0){ if(years > 0 && years >= (- getYear())){ //从公元前到公元年年份加1,0年变成1年 years ++; } else if(years < 0 && (- years) >= getYear()){ //从公元年到公元前年份减1,0年变成公元前1年 years --; }}运行刚刚的测试函数顺利通过,但是运行其他的测试函数出现了新的错误,通过调试发现原因就是上面的修改造成的,因此恢复上面的修改内容。继续运行未通过的测试调试,未能确定修改的位置。然后找了一些类似的测试但是正确通过的,调试运行,发现一些其他的测试通过时执行了条件的另一个分支,因此怀疑其中的一个分支代码存在问题,尝试以下的修复:
instant = iGregorianField.add(instant, value - 1); //原本是value,修改成了value-1运行刚刚的测试同样通过了,但是又出现了新的测试错误同样由上面的修改引起,因此修改也是错的。根据测试的对比以及代码和理解注释内容,最终未能确定修改的位置。修复失败
标准patch:
+++ b/src/main/java/org/joda/time/chrono/GJChronology.java -193,10 +193,6 public final class GJChronology extends AssembledChronology { cutoverInstant = DEFAULT_CUTOVER; } else { cutoverInstant = gregorianCutover.toInstant();- LocalDate cutoverDate = new LocalDate(cutoverInstant.getMillis(), GregorianChronology.getInstance(zone));- if (cutoverDate.getYear() <= 0) {- throw new IllegalArgumentException("Cutover too early. Must be on or after 0001-01-01.");- } } GJChronology chrono; -980,17 +976,6 public final class GJChronology extends AssembledChronology { if (instant < iCutover) { // Only adjust if gap fully crossed. if (instant + iGapDuration < iCutover) {- if (iConvertByWeekyear) {- int wyear = iGregorianChronology.weekyear().get(instant);- if (wyear <= 0) {- instant = iGregorianChronology.weekyear().add(instant, -1);- }- } else {- int year = iGregorianChronology.year().get(instant);- if (year <= 0) {- instant = iGregorianChronology.year().add(instant, -1);- }- } instant = gregorianToJulian(instant); } } -1013,17 +998,6 public final class GJChronology extends AssembledChronology { if (instant < iCutover) { // Only adjust if gap fully crossed. if (instant + iGapDuration < iCutover) {- if (iConvertByWeekyear) {- int wyear = iGregorianChronology.weekyear().get(instant);- if (wyear <= 0) {- instant = iGregorianChronology.weekyear().add(instant, -1);- }- } else {- int year = iGregorianChronology.year().get(instant);- if (year <= 0) {- instant = iGregorianChronology.year().add(instant, -1);- }- } instant = gregorianToJulian(instant); }
bug说明:在对时间进行解析的时候错误的修改了当前的时间,导致解析错误
修复过程描述:(源代码,用时30mins,涉及单个函数)
单步调试未通过的测试,在单步跟踪的过程中发现解析的日期是2月29日会被认为是不合法的,因此反过来看当前的输入信息是2014年,应该是闰年,所以应该是最大值的获取或者年份解析有问题。通过调试发现2014年在解析的过程中转换成了2013年,导致错误的。然后看年份解析过程在解析之前对时间进行了一次调差long instantLocal = instantMillis + chrono.getZone().getOffset(instantMillis);。开始以为是将时间调整到标准时间,但是发现时间格式化类中的Zone是null,再仔细理解发现其中存在问题,在调整时间差的时候用的是自己本身与标准时间的时差,然后在获取年份的时候用的还是自己本身的时区,而时间却用了标准时区的时间,自相矛盾。修改方法是转换全部时间到目标时区进行计算,但是根据测试只给了Locale信息,获取不到TimeZone信息。因此采取了简单的修复方案,直接在原来的时区进行计算,代码如下。可以通过测试。
long instantMillis = instant.getMillis();Chronology chrono = instant.getChronology();long instantLocal = instantMillis + chrono.getZone().getOffset(instantMillis);chrono = selectChronology(chrono);// int defaultYear = chrono.year().get(instantLocal); //原始代码int defaultYear = chrono.year().get(instantMillis); //修复代码标准patch:
Chronology chrono = instant.getChronology();- int defaultYear = DateTimeUtils.getChronology(chrono).year().get(instantMillis); long instantLocal = instantMillis + chrono.getZone().getOffset(instantMillis); chrono = selectChronology(chrono);+ int defaultYear = chrono.year().get(instantLocal);
IllegalArgumentException(3)h==0,结果的符号和m的符号保持一致(4)h<0,结果的符号为负bug说明:时间转换的时候关于符号的处理判断错误
修复过程描述:(源代码+注释,用时20mins,涉及单个函数)
根据未通过的测试函数跟踪调试发现当输入的参数是hour=0,minutes=-xx时,会出现判断分钟不合法。而结合测试的assert信息,其输出应该满足保持符号正确解析。因此对原来的程序进行下面的修改:
//if (minutesOffset < 0 || minutesOffset > 59) { //原程序中的判断条件if (minutesOffset < -59 || minutesOffset > 59) { //修改之后的程序中的判断条件 throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);}再次运行测试发现原来的assert通过了,但是另一个assert出错。观察输入是当hour>0,minutes<0时期望的结果是抛出异常。结合注释信息-23<=hour<=23,-59<=minutes<=59,确定上面的修改没有错误,但是根据注释中提供的示例:
* Hour Minute Example Result * * +ve +ve (2, 15) +02:15 * +ve zero (2, 0) +02:00 * +ve -ve (2, -15) IllegalArgumentException * * zero +ve (0, 15) +00:15 * zero zero (0, 0) +00:00 * zero -ve (0, -15) -00:15 * * -ve +ve (-2, 15) -02:15 * -ve zero (-2, 0) -02:00 * -ve -ve (-2, -15) -02:15通过对上面的示例进行分类总结发现除了上面的hour,minutes的bound外,当输入的参数满足hour>0 && minutes<0时也会出现异常。因此对上面的修复进行修改如下:
if (minutesOffset < -59 || minutesOffset > 59) { throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);}if(hoursOffset > 0 && minutesOffset < 0){ throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);}再次运行测试,结果又出现了新的assert错误,-2, -15被解析成了-1:45,正确的结果是-2:15。同样根据注释信息以及程序的计算过程(将小时换算成分钟再计算),比较上面的注释信息发现当hour<0的时候minutes的符号对结果是没有影响的,然而在计算的过程中涉及到应该加还是减。所以根据注释信息很容易分析出,当minutes>0时应该做减法,因此对该函数的另一处计算过程修改如下,成功通过所有测试。
int hoursInMinutes = hoursOffset * 60;//if (hoursInMinutes < 0) {//原来的程序if (hoursInMinutes < 0 && minutesOffset > 0) { //修复之后的程序 minutesOffset = hoursInMinutes - minutesOffset;} else { minutesOffset = hoursInMinutes + minutesOffset;}标准patch:
-276,17 +276,14 public abstract class DateTimeZone implements Serializable { if (hoursOffset < -23 || hoursOffset > 23) { throw new IllegalArgumentException("Hours out of range: " + hoursOffset); }- if (minutesOffset < -59 || minutesOffset > 59) {+ if (minutesOffset < 0 || minutesOffset > 59) { throw new IllegalArgumentException("Minutes out of range: " + minutesOffset); }- if (hoursOffset > 0 && minutesOffset < 0) {- throw new IllegalArgumentException("Positive hours must not have negative minutes: " + minutesOffset);- } int offset = 0; try { int hoursInMinutes = hoursOffset * 60; if (hoursInMinutes < 0) {- minutesOffset = hoursInMinutes - Math.abs(minutesOffset);+ minutesOffset = hoursInMinutes - minutesOffset; } else { minutesOffset = hoursInMinutes + minutesOffset; }
hourOffset的合法取值范围是-23~23(2)测试输入的参数是24,期望的结果是抛出IllegalArgumentExceptionbug说明:对时间解析时小时不能超过-23~23范围
修复过程描述:(代码+注释,用时2mins,涉及单个函数)
根据未通过测试,发现当输入的时间小时为24时应该抛出异常。单步调试发现在解析函数中没有对该值进行判断。根据注释提供的说明,输入的参数hour应该在-23~23范围之内。因此很容易生成下面的修复代码,成功通过测试。【注意这个和上面的Time-8是同一个函数错误,但是其函数的要求已经不一样了。在Time-8中minutes是可以为负数的,而在Time-9中注释中说明必须在0-59之间,因此函数的计算部分的判断也不一样了】
if(hoursOffset < -23 || hoursOffset > 23){ throw new IllegalArgumentException("Hours out of range: " + hoursOffset);}标准patch:
-255,19 +255,16 public abstract class DateTimeZone implements Serializable { if (hoursOffset == 0 && minutesOffset == 0) { return DateTimeZone.UTC; }- if (hoursOffset < -23 || hoursOffset > 23) {- throw new IllegalArgumentException("Hours out of range: " + hoursOffset);- } if (minutesOffset < 0 || minutesOffset > 59) { throw new IllegalArgumentException("Minutes out of range: " + minutesOffset); } int offset = 0; try {- int hoursInMinutes = hoursOffset * 60;+ int hoursInMinutes = FieldUtils.safeMultiply(hoursOffset, 60); if (hoursInMinutes < 0) {- minutesOffset = hoursInMinutes - minutesOffset;+ minutesOffset = FieldUtils.safeAdd(hoursInMinutes, -minutesOffset); } else {- minutesOffset = hoursInMinutes + minutesOffset;+ minutesOffset = FieldUtils.safeAdd(hoursInMinutes, minutesOffset); } offset = FieldUtils.safeMultiply(minutesOffset, DateTimeConstants.MILLIS_PER_MINUTE); } catch (ArithmeticException ex) {
bug说明:在比较两个日期时由于没有年份信息,默认为1970年,导致2月29日出错。
修复过程描述:(代码+注释,用时1h,涉及多个函数)
根据测试输出的错误信息,定位到当判断日期时对2月29日判断出错。单步调试发现在计算的过程中没有涉及到年份的信息,导致默认年份是1970年,平年没有2月29日。但是根据代码知道其目的只是为了比较两天的时间差,并没有指明具体是哪年。因此2月29日在不指定年份的情况下根据测试应该是合理的,不应该产生异常。因此想到的解决方法是设置年份信息。但是没有找到设置的位置。根据调试的过程发现其中调用了很多公共的接口函数,因此不能对获取月份的最大日期进行修改,否则可能会导致其他的错误。修改方案应该是在最初调用的几层涉及到ReadablePartial类的函数中进行修改,因为在这些函数中能够获取到是否有年信息。而根据函数中引用的变量情况最后确认只存在两个函数中可以修改,一个是between()函数,另一个是set函数,其传入的变量都是ReadablePartial,再继续跟踪其他函数不能获取到调用该函数时日期中是否含有年份信息。结合抛出异常的位置,最后选择了距离异常最近的函数set进行修改,修改方法就是对其进行特殊判断:如果没有年份信息,就设置一个闰年,但是根据代码没有设置年份的接口。阅读代码知道该函数是计算了当前日期到1970年的时间差,采用了一种比较取巧的方法,修改代码如下:
if(!partial.isSupported(DateTimeFieldType.year())){ instant = partial.getFieldType(i).getField(this).set(instant, partial.getValue(i));}//修复的代码boolean flag = false;if(!partial.isSupported(DateTimeFieldType.year())){ //当前的部分时间表示中不包含年份信息 flag = true;}for (int i = 0, isize = partial.size(); i < isize; i++) { if(flag && partial.getValue(i) == 29 && partial.getFieldType(i) == DateTimeFieldType.dayOfMonth()){ //如果是29日就先减掉一天,然后再把这天添加回来 instant = partial.getFieldType(i).getField(this) .set(instant, partial.getValue(i) - 1); instant = instant + DateTimeConstants.MILLIS_PER_DAY; }else{ instant = partial.getFieldType(i).getField(this).set(instant, partial.getValue(i)); }}采用上面的方法测试没有异常产生,但是最后的结果不正确,因为系统默认的年份信息并没有改变,当把时间添加回来之后导致最后结果2月29日比2月1日大一个月,原因是在1970年的2月28日基础之上加上了一天的时间导致系统认为是1970年3月1日。所以修改不正确,看来只能通过修改当前的年份信息。而修改之处只能是set函数和between函数,又set函数不能设置年份信息,最终将修改位置定位到between函数中。又仔细阅读代码发现在调用set函数时传入的参数有两个,根据注释说明,第二个参数是用来被更新的,相当于目前的基准时间。根据set函数的执行过程了解到计算时获取到的时间差是在传入的参数基础之上更新的,目前传入的参数是0L,也就是1970年起始时刻。因此想到如果将传入的参数设置到一个闰年的时间差就相当于在闰年计算。而如果获取这个时间呢?根据之前的调试知道set函数是获取对应日期的millis表示,所以将其复制到了between函数中进行相应的修改就可以了。故最后的修复如下,成功通过测试。
//修改之前代码Chronology chrono = DateTimeUtils.getChronology(start.getChronology()).withUTC();int[] values = chrono.get(zeroInstance, chrono.set(start, 0L), chrono.set(end, 0L));//修改之后代码Chronology chrono = DateTimeUtils.getChronology(start.getChronology()).withUTC();long begin = 0L;if(! start.isSupported(DateTimeFieldType.year())){ begin = DateTimeFieldType.year().getField(chrono).set(0L, 2000); //设置在2000年基础之上计算}int[] values = chrono.get(zeroInstance, chrono.set(start, begin), chrono.set(end, begin));标准patch:
/** The start of 1972. */- private static final long START_1972 = 2L * 365L * 86400L * 1000L; /** The period in the units of this period. */ private volatile int iPeriod; -102,7 +101,7 public abstract class BaseSingleFieldPeriod throw new IllegalArgumentException("ReadablePartial objects must be contiguous"); } Chronology chrono = DateTimeUtils.getChronology(start.getChronology()).withUTC();- int[] values = chrono.get(zeroInstance, chrono.set(start, START_1972), chrono.set(end, START_1972));+ int[] values = chrono.get(zeroInstance, chrono.set(start, 0L), chrono.set(end, 0L)); return values[0];