From b951a180f8f247d809ba4358d409460192c67416 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Thu, 6 Dec 2012 23:56:22 +0100 Subject: [PATCH] Fix quality flaws --- .../dashboards/GlobalDefaultDashboard.java | 36 ++++---- .../core/widgets/ActionPlansWidget.java | 2 +- .../core/widgets/MeasureFilterListWidget.java | 3 +- .../{actionPlans => }/action_plans.html.erb | 0 .../GlobalDefaultDashboardTest.java | 22 ++++- .../plugins/core/widgets/CoreWidgetsTest.java | 6 +- .../core/measure/MeasureFilterCondition.java | 16 +--- .../core/persistence/DatabaseMigrator.java | 14 +--- .../measure/MeasureFilterConditionTest.java | 82 +++++++++++++++++++ .../dialect/OracleSequenceGeneratorTest.java | 12 ++- .../RegisterNewMeasureFiltersTest.java | 14 ++-- .../RenameDeprecatedPropertyKeysTest.java | 51 ++++++++++++ 12 files changed, 200 insertions(+), 58 deletions(-) rename plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/{actionPlans => }/action_plans.html.erb (100%) create mode 100644 sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterConditionTest.java create mode 100644 sonar-server/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java index d58987ec998..c0719a2c898 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java @@ -46,34 +46,40 @@ public final class GlobalDefaultDashboard extends DashboardTemplate { @Override public Dashboard createDashboard() { Dashboard dashboard = Dashboard.create() - .setGlobal(true) - .setLayout(DashboardLayout.TWO_COLUMNS); + .setGlobal(true) + .setLayout(DashboardLayout.TWO_COLUMNS); dashboard.addWidget(WelcomeWidget.ID, 1); + addMyFavouritesWidget(dashboard); + addProjectsWidgets(dashboard); + return dashboard; + } + + private void addMyFavouritesWidget(Dashboard dashboard) { MeasureFilterDto filter = findSystemFilter(MyFavouritesFilter.NAME); if (filter != null) { dashboard - .addWidget(MeasureFilterListWidget.ID, 1) - .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) - .setProperty(MeasureFilterListWidget.PAGE_SIZE_PROPERTY, "50"); + .addWidget(MeasureFilterListWidget.ID, 1) + .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) + .setProperty(MeasureFilterListWidget.PAGE_SIZE_PROPERTY, "50"); } + } - filter = findSystemFilter(ProjectFilter.NAME); + private void addProjectsWidgets(Dashboard dashboard) { + MeasureFilterDto filter = findSystemFilter(ProjectFilter.NAME); if (filter != null) { dashboard - .addWidget(MeasureFilterListWidget.ID, 2) - .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) - .setProperty(MeasureFilterListWidget.PAGE_SIZE_PROPERTY, "20"); + .addWidget(MeasureFilterListWidget.ID, 2) + .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) + .setProperty(MeasureFilterListWidget.PAGE_SIZE_PROPERTY, "20"); dashboard - .addWidget(MeasureFilterTreemapWidget.ID, 2) - .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) - .setProperty(MeasureFilterTreemapWidget.SIZE_METRIC_PROPERTY, "ncloc") - .setProperty(MeasureFilterTreemapWidget.COLOR_METRIC_PROPERTY, "violations_density"); + .addWidget(MeasureFilterTreemapWidget.ID, 2) + .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) + .setProperty(MeasureFilterTreemapWidget.SIZE_METRIC_PROPERTY, "ncloc") + .setProperty(MeasureFilterTreemapWidget.COLOR_METRIC_PROPERTY, "violations_density"); } - - return dashboard; } @Override diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/ActionPlansWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/ActionPlansWidget.java index 6964ec72987..f64f68ad1a1 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/ActionPlansWidget.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/ActionPlansWidget.java @@ -24,6 +24,6 @@ import org.sonar.api.web.WidgetCategory; @WidgetCategory({"Action plans", "Reviews"}) public class ActionPlansWidget extends CoreWidget { public ActionPlansWidget() { - super("action_plans", "Action plans", "/org/sonar/plugins/core/widgets/actionPlans/action_plans.html.erb"); + super("action_plans", "Action plans", "/org/sonar/plugins/core/widgets/action_plans.html.erb"); } } \ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/MeasureFilterListWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/MeasureFilterListWidget.java index a5f4e4612c5..a551f35d74e 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/MeasureFilterListWidget.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/MeasureFilterListWidget.java @@ -39,7 +39,6 @@ public class MeasureFilterListWidget extends CoreWidget { public static final String ID = "measure_filter_list"; public MeasureFilterListWidget() { - super(ID, "Measure Filter as List", - "/org/sonar/plugins/core/widgets/measure_filter_list.html.erb"); + super(ID, "Measure Filter as List", "/org/sonar/plugins/core/widgets/measure_filter_list.html.erb"); } } diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/actionPlans/action_plans.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/action_plans.html.erb similarity index 100% rename from plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/actionPlans/action_plans.html.erb rename to plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/action_plans.html.erb diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java index 2081d92c499..83772b25ac0 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java @@ -61,11 +61,11 @@ public class GlobalDefaultDashboardTest { @Test public void should_create_global_dashboard_with_four_widgets() { when(dao.findSystemFilterByName(MyFavouritesFilter.NAME)).thenReturn( - new MeasureFilterDto().setId(100L) - ); + new MeasureFilterDto().setId(100L) + ); when(dao.findSystemFilterByName(ProjectFilter.NAME)).thenReturn( - new MeasureFilterDto().setId(101L) - ); + new MeasureFilterDto().setId(101L) + ); Dashboard dashboard = template.createDashboard(); List firstColumn = dashboard.getWidgetsOfColumn(1); assertThat(firstColumn).hasSize(2); @@ -80,4 +80,18 @@ public class GlobalDefaultDashboardTest { assertThat(secondColumn.get(1).getId()).isEqualTo(MeasureFilterTreemapWidget.ID); assertThat(secondColumn.get(1).getProperty("filter")).isEqualTo("101"); } + + @Test + public void should_not_fail_if_filter_widgets_not_found() { + when(dao.findSystemFilterByName(MyFavouritesFilter.NAME)).thenReturn(null); + when(dao.findSystemFilterByName(ProjectFilter.NAME)).thenReturn(null); + + Dashboard dashboard = template.createDashboard(); + List firstColumn = dashboard.getWidgetsOfColumn(1); + assertThat(firstColumn).hasSize(1); + assertThat(firstColumn.get(0).getId()).isEqualTo(WelcomeWidget.ID); + + List secondColumn = dashboard.getWidgetsOfColumn(2); + assertThat(secondColumn).isEmpty(); + } } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/widgets/CoreWidgetsTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/widgets/CoreWidgetsTest.java index 19393c20e11..831c922268f 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/widgets/CoreWidgetsTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/widgets/CoreWidgetsTest.java @@ -72,14 +72,16 @@ public class CoreWidgetsTest { @Test public void should_find_templates() { for (CoreWidget widget : widgets()) { - assertThat(widget.getClass().getResource(widget.getTemplatePath())).isNotNull(); + assertThat(widget.getClass().getResource(widget.getTemplatePath())) + .as("Template not found: " + widget.getTemplatePath()) + .isNotNull(); } } @Test public void should_be_registered_as_an_extension() { for (CoreWidget widget : widgets()) { - assertThat(new CorePlugin().getExtensions()).contains(widget.getClass()); + assertThat(new CorePlugin().getExtensions()).as("Widget not registered: " + widget.getClass()).contains(widget.getClass()); } } diff --git a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterCondition.java b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterCondition.java index 2485c905583..746902aff25 100644 --- a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterCondition.java +++ b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterCondition.java @@ -35,10 +35,6 @@ public class MeasureFilterCondition { this.sql = sql; } - public String getCode() { - return code; - } - public String getSql() { return sql; } @@ -51,15 +47,6 @@ public class MeasureFilterCondition { } throw new IllegalArgumentException("Unknown operator code: " + code); } - - public static Operator fromSql(String sql) { - for (Operator operator : values()) { - if (operator.sql.equals(sql)) { - return operator; - } - } - throw new IllegalArgumentException("Unknown operator sql: " + sql); - } } private final Metric metric; @@ -101,10 +88,11 @@ public class MeasureFilterCondition { return "pm.value"; } - void appendSqlCondition(StringBuilder sql) { + StringBuilder appendSqlCondition(StringBuilder sql) { sql.append(" pm.metric_id="); sql.append(metric.getId()); sql.append(" AND ").append(valueColumn()).append(operator.getSql()).append(value); + return sql; } @Override diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java index dba97bb0a70..3211cc55c39 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java @@ -57,17 +57,11 @@ public class DatabaseMigrator implements ServerComponent { connection = session.getConnection(); DdlUtils.createSchema(connection, database.getDialect().getId()); } finally { - try { - MyBatis.closeQuietly(session); + MyBatis.closeQuietly(session); - // The connection is probably already closed by session.close() - // but it's not documented in mybatis javadoc. - if (null != connection) { - connection.close(); - } - } catch (Exception e) { - // ignore - } + // The connection is probably already closed by session.close() + // but it's not documented in mybatis javadoc. + DatabaseUtils.closeQuietly(connection); } return true; } diff --git a/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterConditionTest.java b/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterConditionTest.java new file mode 100644 index 00000000000..112975ff908 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/measure/MeasureFilterConditionTest.java @@ -0,0 +1,82 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.core.measure; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric; + +import static org.fest.assertions.Assertions.assertThat; + +public class MeasureFilterConditionTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void create_operator_from_code() { + assertThat(MeasureFilterCondition.Operator.fromCode("eq")).isEqualTo(MeasureFilterCondition.Operator.EQUALS); + assertThat(MeasureFilterCondition.Operator.fromCode("lte")).isEqualTo(MeasureFilterCondition.Operator.LESS_OR_EQUALS); + } + + @Test + public void fail_if_operator_code_not_found() { + thrown.expect(IllegalArgumentException.class); + MeasureFilterCondition.Operator.fromCode("xxx"); + } + + @Test + public void operator_sql() { + assertThat(MeasureFilterCondition.Operator.EQUALS.getSql()).isEqualTo("="); + assertThat(MeasureFilterCondition.Operator.LESS_OR_EQUALS.getSql()).isEqualTo("<="); + assertThat(MeasureFilterCondition.Operator.GREATER.getSql()).isEqualTo(">"); + } + + @Test + public void value_condition() { + Metric ncloc = new Metric.Builder("ncloc", "NCLOC", Metric.ValueType.INT).create(); + ncloc.setId(123); + MeasureFilterCondition condition = new MeasureFilterCondition(ncloc, MeasureFilterCondition.Operator.GREATER, 10.0); + + assertThat(condition.metric()).isEqualTo(ncloc); + assertThat(condition.operator()).isEqualTo(MeasureFilterCondition.Operator.GREATER); + assertThat(condition.period()).isNull(); + assertThat(condition.value()).isEqualTo(10.0); + assertThat(condition.valueColumn()).isEqualTo("pm.value"); + assertThat(condition.toString()).isNotEmpty(); + assertThat(condition.appendSqlCondition(new StringBuilder()).toString()).isEqualTo(" pm.metric_id=123 AND pm.value>10.0"); + } + + @Test + public void variation_condition() { + Metric ncloc = new Metric.Builder("ncloc", "NCLOC", Metric.ValueType.INT).create(); + ncloc.setId(123); + MeasureFilterCondition condition = new MeasureFilterCondition(ncloc, MeasureFilterCondition.Operator.LESS_OR_EQUALS, 10.0); + condition.setPeriod(3); + + assertThat(condition.metric()).isEqualTo(ncloc); + assertThat(condition.operator()).isEqualTo(MeasureFilterCondition.Operator.LESS_OR_EQUALS); + assertThat(condition.period()).isEqualTo(3); + assertThat(condition.value()).isEqualTo(10.0); + assertThat(condition.valueColumn()).isEqualTo("pm.variation_value_3"); + assertThat(condition.toString()).isNotEmpty(); + assertThat(condition.appendSqlCondition(new StringBuilder()).toString()).isEqualTo(" pm.metric_id=123 AND pm.variation_value_3<=10.0"); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/dialect/OracleSequenceGeneratorTest.java b/sonar-core/src/test/java/org/sonar/core/persistence/dialect/OracleSequenceGeneratorTest.java index 9333cb0dd53..e50efdac844 100644 --- a/sonar-core/src/test/java/org/sonar/core/persistence/dialect/OracleSequenceGeneratorTest.java +++ b/sonar-core/src/test/java/org/sonar/core/persistence/dialect/OracleSequenceGeneratorTest.java @@ -24,8 +24,7 @@ import org.junit.Test; import java.util.Properties; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.fest.assertions.Assertions.assertThat; public class OracleSequenceGeneratorTest { @@ -37,7 +36,14 @@ public class OracleSequenceGeneratorTest { OracleSequenceGenerator generator = new OracleSequenceGenerator(); generator.configure(null, props, new Oracle.Oracle10gWithDecimalDialect()); - assertThat(generator.getSequenceName(), is("MY_TABLE_SEQ")); + assertThat(generator.getSequenceName()).isEqualTo("MY_TABLE_SEQ"); } + @Test + public void should_not_fail_if_table_name_can_not_be_loaded() { + Properties props = new Properties(); + OracleSequenceGenerator generator = new OracleSequenceGenerator(); + generator.configure(null, props, new Oracle.Oracle10gWithDecimalDialect()); + assertThat(generator.getSequenceName()).isNotEmpty(); + } } diff --git a/sonar-server/src/test/java/org/sonar/server/startup/RegisterNewMeasureFiltersTest.java b/sonar-server/src/test/java/org/sonar/server/startup/RegisterNewMeasureFiltersTest.java index 69c881273bc..efe5a492a1f 100644 --- a/sonar-server/src/test/java/org/sonar/server/startup/RegisterNewMeasureFiltersTest.java +++ b/sonar-server/src/test/java/org/sonar/server/startup/RegisterNewMeasureFiltersTest.java @@ -40,7 +40,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class RegisterNewMeasureFiltersTest { - private RegisterNewMeasureFilters registerMeasure; + private RegisterNewMeasureFilters registration; private MeasureFilterDao filterDao; private LoadedTemplateDao loadedTemplateDao; private FilterTemplate filterTemplate; @@ -51,7 +51,7 @@ public class RegisterNewMeasureFiltersTest { loadedTemplateDao = mock(LoadedTemplateDao.class); filterTemplate = mock(FilterTemplate.class); - registerMeasure = new RegisterNewMeasureFilters(new FilterTemplate[]{filterTemplate}, filterDao, loadedTemplateDao); + registration = new RegisterNewMeasureFilters(new FilterTemplate[]{filterTemplate}, filterDao, loadedTemplateDao); } @Test @@ -59,7 +59,7 @@ public class RegisterNewMeasureFiltersTest { when(loadedTemplateDao.countByTypeAndKey(eq(LoadedTemplateDto.FILTER_TYPE), anyString())).thenReturn(0); when(filterTemplate.createFilter()).thenReturn(Filter.create()); - registerMeasure.start(); + registration.start(); verify(filterDao).insert(any(MeasureFilterDto.class)); verify(loadedTemplateDao).insert(any(LoadedTemplateDto.class)); @@ -69,7 +69,7 @@ public class RegisterNewMeasureFiltersTest { public void should_insert_nothing_if_templates_are_alreday_loaded() { when(loadedTemplateDao.countByTypeAndKey(eq(LoadedTemplateDto.FILTER_TYPE), anyString())).thenReturn(1); - registerMeasure.start(); + registration.start(); verify(filterDao, never()).insert(any(MeasureFilterDto.class)); verify(loadedTemplateDao, never()).insert(any(LoadedTemplateDto.class)); @@ -79,7 +79,7 @@ public class RegisterNewMeasureFiltersTest { public void should_register_filter() { when(filterTemplate.createFilter()).thenReturn(Filter.create()); - MeasureFilterDto filterDto = registerMeasure.register("Fake", filterTemplate.createFilter()); + MeasureFilterDto filterDto = registration.register("Fake", filterTemplate.createFilter()); assertThat(filterDto).isNotNull(); verify(filterDao).insert(filterDto); @@ -90,7 +90,7 @@ public class RegisterNewMeasureFiltersTest { public void should_not_recreate_filter() { when(filterDao.findSystemFilterByName("Fake")).thenReturn(new MeasureFilterDto()); - MeasureFilterDto filterDto = registerMeasure.register("Fake", null); + MeasureFilterDto filterDto = registration.register("Fake", null); assertThat(filterDto).isNull(); verify(filterDao, never()).insert(filterDto); @@ -107,7 +107,7 @@ public class RegisterNewMeasureFiltersTest { .add(FilterColumn.create("metric", "distance", "ASC", false)) ); - MeasureFilterDto dto = registerMeasure.createDtoFromExtension("Fake", filterTemplate.createFilter()); + MeasureFilterDto dto = registration.createDtoFromExtension("Fake", filterTemplate.createFilter()); assertThat(dto.getName()).isEqualTo("Fake"); assertThat(dto.isShared()).isTrue(); diff --git a/sonar-server/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java b/sonar-server/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java new file mode 100644 index 00000000000..06ba9b9e3bb --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/startup/RenameDeprecatedPropertyKeysTest.java @@ -0,0 +1,51 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.startup; + +import org.junit.Test; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.core.properties.PropertiesDao; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class RenameDeprecatedPropertyKeysTest { + @Test + public void should_rename_deprecated_keys() { + PropertiesDao dao = mock(PropertiesDao.class); + PropertyDefinitions definitions = new PropertyDefinitions(FakeExtension.class); + RenameDeprecatedPropertyKeys task = new RenameDeprecatedPropertyKeys(dao, definitions); + task.start(); + + verify(dao).renamePropertyKey("old_key", "new_key"); + verifyNoMoreInteractions(dao); + } + + @Properties({ + @Property(key = "new_key", deprecatedKey = "old_key", name = "Name"), + @Property(key = "other", name = "Other") + }) + public static class FakeExtension { + + } +} -- 2.39.5